本章讲述何时如何覆盖Object中的nonfinal方法。由于Item7已逃离了finalize,所以本章不再赘述。Comparable.compareTo虽然不是Object中的方法,但是由于它有类似的特性,所以也在本章讨论。
重写equals方法看起来很简单,但是许多重写方式会导致错误,并且后果很严重。如果类的实例只与其自身equals,那么避免这种问题的最简单办法就是不去重写equals方法。只要满足以下任一条件,就可以这样做。
类的每个实例本质上都是唯一的。【例】例如Thread这样的表示活动实体,而非表示值的类。Object类提供的equals实现对于这些类来说恰好拥有正确的行为。
不关心类是否提供“逻辑相等”的测试功能。【例】例如java.util.Random本可以重写equals来检查两个Random实例是否生产了同样的随机数字序列,但是设计者不认为客户端会需要这样的功能。在这些情况下,从Object继承来的equals实现就已经足够了。
父类已经重写equals,并且父类的行为对于子类也是合适的。【例】例如大多数Set的实现类都从AbstractSet继承了equals方法,List的实现类从AbstractList继承了equals方法,Map的实现类从AbstractMap继承了equals方法。
类是私有的,或包级私有,并且确信其equals方法永远不会被调用。Arguably,其equals方法是应该重写成如下形式,以防止被意外调用:
[java]
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
那么何时应该重写Object.equals呢?当类拥有“逻辑相等”的概念,并且其父类并未重写equals以实现期望的行为,这时就需要重写Object.equals。对于值类(value classes)通常就是这种情况,所谓值类是指表示“值”的类,例如Integer或Date。程序员通过equals方法判断值类的实例是否逻辑相等,而不是判断他们是否指向相同的对象。重写equals方法不仅是为了满足程序员的要求,同时也使得这些实例可以作为map的key,或者是set的元素,并拥有可预见的、期望中的行为。
有一种值类却不需要重写equals方法:实例受控的类(Item1),其每个值至多存在一个对象。【例】Enum类型就是这种值类。对于这些类,逻辑相等就等同于对象相等,所以Object.equals方法的功能就等同于逻辑equals方法。
当你重写equals方法时,必须遵守其通用约定。如下是从Object的规范中拷贝来的约定内容:
equals方法实现了等价关系(equivalence relation):
自反性(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。
非空性(Non-Nullity):对于任何非null的引用x,x.equals(null)必须返回false。
1)自反性(Reflexive)
第一个要求是说对象必须等于其本身。很难想象会无意中违反这一条约定。如果你违反该约定,把实例添加到集合后,集合的contains方法会告诉你该集合不包含你刚刚添加的实例。
2)对称性(Symmetric)
第二个要求是说两个对象必须对于“他们是否相等”保持一致。与第一条不同,不难想象无意违反此条要求的情形。【例】例如下面这个类,实现了一个大小写敏感的字符串。字符串的大小写在toString中保存,但在比较时却忽略了。
[java]
// Broken - violates symmetry!
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String) o);
return false;
}
equals方法的意图很好,天真地试图与普通字符串进行互操作。假设有如下两个对象:
[java]
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s)返回true,但是s.equals(cis)却返回false,显然违反了对称性。假设你将其放入集合中:
[java]
List list = new ArrayList();
list.add(cis);
list.contains(s)会返回什么呢?没人知道!在Sun的当前实现中,碰巧返回false,但是在其他实现中,也可能返回true或者抛出一个运行时异常。一旦你违反equals的约定,当其他对象面对你的对象时,你就无法知道这些对象的行为会是怎么样的。
【解决办法】