覆盖equals方法的权宜之计,组合优于继承
当你创建自己的类型,一种有效的建议就是去重新实现那些属于Object类的一些方法——toString()、equals()、hashcode()。没错,这些都是正确的建议。但是,对于这些方法有时我们并不能很有效地去为我们的类给出高效的实现,比如说eqauls()。
假设有一个类,point:
可以看到,我们在这个类中,有两个成员变量,分别用来存储Point的x坐标和y坐标。并且,我们覆盖了基类的equals(),给出了我们自己的实现,这没有太大的问题。
然后,我们考虑到,有些点是有颜色的。然后,我们需要一个ColorPoint继承Point,并对其加以扩展。
可以看到,在代码中,我们定义了一个ColorPoint1类,继承自Point。同时增加了一个值组件(Color)。然后,我们考虑,该如何实现它自己的equals方法呢?
实现1:比较所有的值域:x,y,color,也就是说,只有坐标相同,并且颜色也相同,我们才认为这样的点是相同的。实现如下:
没错,看似正确的逻辑。
下面我们创建两个点:
Point p1=new Point(1,2);
ColorPoint1 c1=new ColorPoint1 (1,2,"red");
你会发现c1.equals(p1)和p1.equals(c1);返回值居然不同。前者的比较逻辑正如ColorPoint1 的equals()那样,首先p1不是ColorPoint1 的实例,它没有color字段,所以这样的比较总是返回false。而后一个的比较逻辑如Point的equals(),它仅仅比较坐标值,所以它会返回true.
有人会问,这有什么问题。这确实有问题,它违反了实现equals()方法的规范之一——对称性。即:x.equals(y)返回值应该等于y.equals(x)的返回值。如果违反这样的约定,总是会出现问题。
当然,你可以在进行混合比较的时候,忽略颜色值:
这个能够满足对称性,但是却牺牲了传递性。
ColorPoint1 c1=new ColorPoint1 (1,2,"red");
Point p1=new Point(1,2);
ColorPoint1 c2=new ColorPoint1 (1,2,"blue");
c1.equals(p1)返回true,p1.equals(c1)返回true,但是c1.equals(c2)却返回false.
你当然可以在ColorPoint的equals的实现逻辑上,仍然沿用Point的机制——只比较他们的坐标值,但是这样的结果确实不能令人接受的。一个红颜色的点怎么能等于一个蓝颜色的点?
由此,你能够看到,一个矛盾的存在——在既想增加值组件,又不想失去抽象和多态特性的情况下,很难把握好子类的equals()方法的实现逻辑。
当然,在《Effective Java》中还是给出了,一个面对这种问题的权益之计:将继承转化为组合。
实现如下:
这种实现,给出了对等对象的比较,也就是在同一类层次的比较,这样就不会出现子类和父类这样跨层次的比较,也就能避免失去对称性和传递性的问题。也就是说,有颜色的坐标点和没有颜色的坐标点始终是不等的。
当然,这里只是给出了面对这种问题的一种解决方案而已。在实际的编码过程中,我们需要去考虑采用继承还是组合,哪种实现更为合理。并且去考虑改变实现方式所付出的代价。
覆盖equals方法的几个规范:自反性、对称性、传递性、一致性。
高效的实现equals()的几个参考:
(1)使用==比较参数是否为当前对象的引用。
(2)使用instanceOf比较参数的类型是否正确。
(3)将类型转化为当前对象的类型
(4)比较所有关键的值域
(5)自我检测是否满足规范。
——摘自《Effective Java》