避免重写 equals 方法
重写equals 方法看起来很简单,但是还会有多种方式导致出错,后果可能是严重的。最简单,最容易避免出错的方式是 避免重写equals方法 ,采用这种方式的每个类只需要和自己对比即可,这样永远不会出错。如果满足了以下任何一个约定,也能产生正确的结果:
1. 该类的每个实例本质上都是唯一的
即使对于像Thread 这种代表活动状态的实体而不是值的类来说也是如此。Object提供的equals方法也能确保这个类展现出正确的行为。
2. 类没有必要提供逻辑相等的测试
例如:java.util.regex.Pattern能够重写equals检查是否两个Pattern 实例是否代表了同一个正则表达式。但是设计者并不认为客户需要或者期望这样的功能。在这种情况下,从Object继承的equals方法的实现就已经足够了。
3. 超类已经重写了equals方法,并且超类的行为对此类也适用
例如:大部分Set实现从AbstractSet那里继承了equals方法,List实现从AbstractList那里继承了equals 方法,Map实现从AbstractMap那里继承了equals 方法。
4. 这个类是私有的或者包级私有的,可以确定equals方法永远不会调用
如果你非常想要规避风险,那就确保equals方法不会突然调用
@Override public boolean equals(Object o){ throw new AssertionError(); }
那么何时重写equals方法呢?
当一个类具有逻辑相等的概念时,它不仅仅是对象身份,而超类还没有覆盖equals,这通常属于值类的情形。一个值类仅仅是一个代表了值的类,例如Integer 或者String。程序员用equals来比较对象的时候,往往想要知道的是两个对象在逻辑上是否相等,而不是想了解他们是否指向同一个对象。为了满足程序员的要求,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以用作映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出正确的行为。
一种不需要重写equals方法的值类是一个使用单例实现类,以确保每个值最多只有一个对象。枚举类型就属于此类别。对于这些类,逻辑相等就是对象相等,所以对象的equals方法判断的相等也表示逻辑相等。
重写equals 遵循的约定
如果你非要重写equals 方法,请遵循以下约定:
- 自反性:对于任何非 null 的引用值 x,x.equals(x),必须返回true,null equals (null) 会有空指针。
- 对称性:对于任何非 null 的引用值 x 和 y,当且仅当 x.equals(y) 为true时,y.equals(x) 时也必须返回true。
- 传递性:对于任何非 null 的引用值 x 、y和 z ,如果 x.equals(y) 为 true 时,y.equals(z) 也是 true 时,那么x.equals(z) 也必须返回 true。
- 一致性:对于任何非 null 的引用值 x 和 y,只要 equals 比较在对象中信息没有修改,多次调用 x.equals(y) 就会一致返回 true,或者一致返回 false。
- 对于任何非 null 的引用值x, x.equals(null) 必须返回false。
解释
现在你已经知道了违反 equals 约定是多么可怕,下面将更细致的讨论,下面我们逐一查看这五个要求
自反性
自反性:第一个要求仅仅说明对象必须等于它自身,假如违背了这一条,然后把该类添加到集合中,该集合的 contains 方法会告诉你,该集合不包含你刚刚添加的实例。
对称性
对称性:这个要求是说,任何两个对象在对于"它们是否相等" 的问题上都必须保持一致。例如如下这个例子
public class CaseInsensitiveString { private final String s; public CaseInsensitiveString(String s){ this.s = Objects.requireNonNull(s); } @Override public boolean equals(Object o) { if(o instanceof CaseInsensitiveString){ return s.equalsIgnoreCase(((CaseInsensitiveString)o).s); } if(o instanceof String){ return s.equalsIgnoreCase((String)o); } return false; } public static void main(String[] args) { CaseInsensitiveString cis = new CaseInsensitiveString("Polish"); String s = "Polish"; System.out.println(cis.equals(s)); System.out.println(s.equals(cis)); } }
不出所料,cris.equals(s) 返回true。问题在于,虽然 CaseInsensitiveString 类的 equals 方法知道普通的字符串对象,但是, String 类中的 equals 方法却并不知道不区分大小写的字符串,因此,s.equals(cris) 返回false,显然违反了对称性。
如果你用下面的示例来进行操作
List<CaseInsensitiveString> list = new ArrayList<>(); list.add(cis); System.out.println(list.contains(s));
会返回什么呢?
没人知道,可能在 OpenJDK 实现中会返回 false,但这只是特定实现的结果而已,在其他的实现中,也有可能返回true,或者抛出运行时异常,所以我们能总结出一点:一旦违反了equals 约定,当面对其他对象时,你完全不知道这些对象的行为会怎么样
为了解决这个问题,那么就需要去掉与 String 互操作的这段代码去掉,变成下面这样
@Override public boolean equals(Object o) { return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s); }
传递性:equals 约定的第三个要求是传递性,如果一个对象等于第二个对象,而第二个对象又等于第三个对象,那么第一个对象一定等于第三个对象。同样的,无意识的违反这条规则的情形也不难,例如
public class Point { private final int x; private final int y; public Point(int x,int y){ this.x = x; this.y = y; } @Override public boolean equals(Object o) { if(!(o instanceof Point)){ return false; } Point p = (Point)o; return p.x == x && p.y == y; } }
假如你想扩展这个类,添加一些颜色信息:
public class ColorPoint extends Point{ private final Color color; public ColorPoint(int x, int y,Color color) { super(x, y); this.color = color; } }
equals 方法是什么样的呢?如果完全不提供equals 方法,而是直接从 Point 继承过来,在 equals 做比较的时候颜色信息就被忽略。虽然这样做不会违反 equals 约定,但这很显然是不可接受的。假设编写了一个 equals 方法,只有当它的参数是一个有色点,并且具有相同位置和颜色时,才会返回true。