重写equals方法的约定

1. 什么时候需要重写Object.equals方法

如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。

这通常属于“值类(value class)”的情形。值类仅仅是一个表示值的类,例如Integer或Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。

有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只有一个对象”的类,例如枚举类型。对于这样的类,逻辑相同与对象等同是一回事。

 

2.equals的通用约定

· 自反性reflexive。对于任何非null的引用值x, x.equals(x)必须返回true;

· 对称性symmetric。对于任何非null的引用值x和y, 当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;

· 传递性transitive。对于任何非null的引用值x, y和z,如果x.equals(y)返回true, 并且 y.equals(z) 返回true,那么x.equals(z)也必须返回true;

· 一致性consistent。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致返回false;因此,equals方法里面不应该依赖任何不可靠的资源

· 对于任何非null的引用值x, x.equals(null)必须返回false

 

3.一些不好的示例

1)违反对称性。

定义如下一个类。

 1 class CaseInsensitiveString {
 2 
 3     private String insensitiveString;
 4 
 5     public CaseInsensitiveString(String string) {
 6         this.insensitiveString = string;
 7     }
 8 
 9     public String getString() {
10         return this.insensitiveString;
11     }
12 
13     @Override
14     public boolean equals(Object object) {
15         if (null == this.insensitiveString) {
16             return false;
17         }
18 
19         if (object instanceof CaseInsensitiveString) {
20             return this.insensitiveString
21                     .equalsIgnoreCase(((CaseInsensitiveString) object)
22                             .getString());
23         }
24 
25         if (object instanceof String) {
26             return this.insensitiveString.equals((String) object);
27         }
28 
29         return false;
30     }
31 }

该类的equals方法,首先违反了对称性。

 1 public class App {
 2 
 3     public static void main(String[] args) {
 4         CaseInsensitiveString cis = new CaseInsensitiveString("whatever");
 5         String string = "whatever";
 6         System.out.println(cis.equals(string));
 7         System.out.println(string.equals(cis));
 8     }
 9 
10 }

返回结果将是true和false。

2)考虑继承的场景。

假设下面有一个圆形的类,只要半径相同,就认为两个圆逻辑上等同。

 1 class Circle {
 2     private int radius;
 3 
 4     public Circle(int radius) {
 5         this.radius = radius;
 6     }
 7 
 8     public int getRadius() {
 9         return this.radius;
10     }
11 
12     @Override
13     public boolean equals(Object object) {
14         if (object instanceof Circle) {
15             return this.radius == ((Circle) object).getRadius();
16         }
17 
18         return false;
19     }
20 }

假如想要扩展这个类,为其加上一个颜色color的属性,那么其子类的equals方法要怎么写呢?

 1 class ColorCircle extends Circle {
 2 
 3     public static enum COLOR {
 4         RED, WHITE, BLUE, GREEN, YELLOW
 5     };
 6 
 7     private COLOR color;
 8 
 9     public COLOR getColor() {
10         return this.color;
11     }
12 
13     public ColorCircle(int radius, COLOR color) {
14         super(radius);
15         this.color = color;
16         // TODO Auto-generated constructor stub
17     }
18 }

 

如果不覆盖equals方法,使用其父类的方法,那么,新添加的属性就会被忽略。虽然这样做不违反约定,但这样显然是无法接受的,否则为什么要新增一个属性?

那么,为了突出新增加的属性,提供如下的equals方法。

1 @Override
2     public boolean equals(Object object) {
3         if (!(object instanceof ColorCircle)) {
4             return false;
5         }
6 
7         return super.equals(object)
8                 && ((ColorCircle) object).getColor() == this.color;
9     }

这样做,违反了对称性。下面实例化了两个对象,一个是以Circle,另一个是带颜色的ColorCircle,很明显,打印出来的结果一个是true,一个是false.

1 public static void main(String[] args) {
2         Circle circle = new Circle(1);
3         ColorCircle colorCircle = new ColorCircle(1, COLOR.BLUE);
4         System.out.println(circle.equals(colorCircle));
5         System.out.println(colorCircle.equals(circle));
6     }

那么也许你会说,这种情况,那就判断其是子类还是父类,采用不同的策略进行比较。

 1 @Override
 2     public boolean equals(Object object) {
 3         if(!(object instanceof Circle)) {
 4             return false;
 5         }
 6         
 7         if (!(object instanceof ColorCircle)) {
 8             return super.equals(object);
 9         }
10         else {
11             return super.equals(object)
12                     && ((ColorCircle) object).getColor() == this.color;
13         }
14     }

这样是解决了对称性的问题了,但又带来了另一个问题,传递性。

1     public static void main(String[] args) {
2         ColorCircle colorCircle = new ColorCircle(1, COLOR.BLUE);
3         Circle circle = new Circle(1);
4         ColorCircle anotherColorCircle = new ColorCircle(1, COLOR.RED);
5         System.out.println(colorCircle.equals(circle));
6         System.out.println(circle.equals(colorCircle));
7         System.out.println(colorCircle.equals(anotherColorCircle));
8     }

如上,colorCircle和circle等同,circle与anotherColorCircle等同,而很明显,colorCircle和anotherColorCircle是不等同的。

事实上,这是面向对象语言中,关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象带来的优势。

注意,这里说的是可实例化的类的扩展,对于抽象类不影响。

当然,你也可以用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:

 

 1     @Override
 2     public boolean equals(Object object) {
 3         if (null == object || object.getClass() != getClass()) {
 4             return false;
 5         }
 6 
 7         ColorCircle colorCircle = (ColorCircle) object;
 8         return this.getRadius() == colorCircle.getRadius()
 9                 && this.color == colorCircle.getColor();
10     }

 

但是违背了里氏替换原则:任何基类出现的地方都可以无差别地使用子类替换。很明显,如果两个Circle等同,此时将其中一个替换为ColorCircle类,则不再等同。因此,子类应尽量少重写父类的方法。

总结一下,父类和子类的这种equals方法的重写问题的原因,首先,是因为父类是一个可实例化的类。那么就可能出现父类对象与子类对象比较的场景。所以对于抽象类来讲,并不影响,因为其无法实例化。

其次,是因为子类增加了一些标识身份的属性,必须在判断等同时使用。那么当父类与子类两个对象进行比较时,就会出现这种冲突,就需要思考,这个属性在比较的时候到底要不要使用。

因此,当【父类可实例化】且【子类新增属性必须参与到equals比较】的时候,equals方法总是会违反某些原则。

一个可以采用的方法是,组合优先于继承。在这里就不展开讨论了。

 

posted @ 2016-11-16 22:29  kingsleylam  阅读(1538)  评论(0编辑  收藏  举报