平时很难遇到需要覆盖equals的情况。
什么时候不需要覆盖equals?
如果要问什么时候需要覆盖equals?答案正好和之前的问题相反。即,类需要一个自己特有的逻辑相等概念,而且超类提供的equals不满足自己的行为。(PS:对于枚举而言,逻辑相等和对象相等都是一回事。)
既然只好覆盖equals,我们就需要遵守一些规定:
其实这些规定随便拿出一个都是很好理解的。难点在于,当我遵守一个规定时有可能违反另一个规定。
自反性就不用说了,很难想想会有人违反这一点。
关于对称性,下面提供一个反面例子:
class CaseInsensitiveString { PRivate final String s; public CaseInsensitiveString(String s) { if (s == null) this.s = StringUtils.EMPTY; else this.s = s; } @Override public boolean equals(Object obj) { if (obj instanceof CaseInsensitiveString) return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s); if (obj instanceof String) return s.equalsIgnoreCase((String) obj); return false; }}
这个例子显然违反对称性,即x.equals(y)为true 但 y.equals(x)为false。不仅是在显示调用时,如果将这种类型作为泛型放到集合之类的地方,会发生难以预料的行为。
而对于上面这个例子,在equals方法中我就不牵扯其他类型,去掉String实例的判断就可以了。
关于传递性,即,当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true。这个规定在对类进行扩展时尤其明显。
比如,我用x,y描述某个Point:
class Point { private final int x; private final int y; public Point(int x, int y) { super(); this.x = x; this.y = y; } @Override public boolean equals(Object obj) { if (!(obj instanceof Point)) return false; Point p = (Point) obj; return p.x == x && p.y == y; }}
现在我想给Point加点颜色:
class ColorPoint extends Point { private final Color color; public ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; } @Override public boolean equals(Object obj) { if (!(obj instanceof ColorPoint)) return false; return super.equals(obj) && ((ColorPoint) obj).color == color; }}
似乎很自然的提供了ColorPoint的equals方法,但他连对称性的没能满足。于是我们加以修改,令其满足对称性:
@Overridepublic boolean equals(Object obj) { if (!(obj instanceof Point)) return false; if (!(obj instanceof ColorPoint)) return obj.equals(this); return super.equals(obj) && ((ColorPoint) obj).color == color;}
好了,接下来我们就该考虑传递性了。比如我们现在有三个实例,1个Point和2个ColorPoint....然后很显然,不满足<当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true>。 事实上,我们无法在扩展可实例化类的同时,既增加新的值组件,又保留equals约定。
于是我索性不用instanceof,改用getClass()。这个确实可以解决问题,但很难令人接受。如果我有一个子类没有覆盖equals,此时equals的结果永远是false。
既然如此,我就放弃继承,改用复合(composition)。以上面的ColorPoint作为例子,将Point变成ColorPoint的field,而不是去扩展。 代码如下:
public class ColorPoint { private final Point point; private final Color color; public ColorPoint(int x, int y, Color color) { if (color == null) throw new NullPointerException(); point = new Point(x, y); this.color = color; } /** * Returns the point-view of this color point. */ public Point aspoint() { return point; } @Override public boolean equals(Object o) { if (!(o instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint) o; return cp.point.equals(point) && cp.color.equals(color); } @Override public int hashCode() { return point.hashCode() * 33 + color.hashCode(); }}
关于一致性,即如果两者相等则始终相等,除非有一方被修改。这一点与其说equals方法,到不如思考写一个类的时候,这个类应该设计成可变还是不可变。如果是不可变的,则需要保证一致性。
考虑到这些规定,以下是重写equals时的一些建议:
任何覆盖了equals方法的类都需要覆盖hashCode方法。忽视这一条将导致类无法与基于散列的数据结构一起正常工作,比如和HashMap、HashSet和Hashtable。
下面是hashCode相关规范:
在程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这个对象调用多少次hashCode,起结果必须始终如一地返回同一个证书。如果是同一个程序执行多次,每次调用的结果可以不一致。
如果两个对象根据equals方法比较是相等的,那么两个对象的hashCode结果必须相同。
如果两个对象根据equals方法比较是不相等的,那么这两个对象的hashCode不一定返回不同的结果。但是,如果不同的对象返回不同的hashCode,则能提高散列表的性能。
下面的代码是一个反面例子:
import java.util.HashMap;import java.util.Map;public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "area code"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "line number"); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber; } private static void rangeCheck(int arg, int max, String name) { if (arg < 0 || arg > max) throw new IllegalArgumentException(name + ": " + arg); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber) o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } // Broken - no hashCode method! // A decent hashCode method - Page 48 // @Override public int hashCode() { // int result = 17; // result = 31 * result + areaCode; // result = 31 * result + prefix; // result = 31 * result + lineNumber; // return result; // } public static void main(String[] args) { Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(m.get(new PhoneNumber(707, 867, 5309))); }}
通过equals方法比较,两个实例在逻辑上是相等的。但由于没有覆盖hashCode方法,两个实例返回的hashCode是不同的。在散列表中,如果散列码不匹配,就不必检查两个实例是否相等。如果随便提供这样的一个hashCode方法:
public int hashCode(){ return 42;}
这样会让散列表失去优势,退化为链表。
最好的hashCode应该是<不同的对象产生不同的散列码>。即,散列函数把集合中不同的实例均匀地分布到所有可能的散列值上。
下面是一种简单的思路(也就是上面例子中注释的部分):
针对每一个关键的field(假设变量名为f)计算int类型的散列码,不同类型有不同的计算方式。
注意,这里仅限关键field。对于那些用其他field值计算出来的field,我们可以将其排除在外。
如果一个类是不可变的,而且计算散列值的开销比较大,我们可以试着将散列值缓存。或者我们也可以试试延迟初始化,在hashCode第一次被调用时进行初始化:
private volatile int hashCode; // (See Item 71)@Override public int hashCode() { int result = hashCode; if (result == 0) { result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; hashCode = result; } return result;}
另外,Josh Bloch在最后加了一段话:
Many classes in the Java platform libraries, such as String, Integer, and Date, include in their specifications the exact value returned by their hashCode method as a function of the instance value. This is generally not a good idea, as it severely limits your ability to improve the hash function in future releases.
<可以把它们的hashCode方法返回的确切值规定为该实例的一个函数。> 看了翻译后一头雾水...
后来在爆栈中看到这么一个回复,记下来作为参考:
The API docs specify that String.hashCode() is computed by a specific formula. Client code is free to independently compute the hash code using that exact formula and assume it will be the same as that returned by String.hashCode(). This might seem perverse for pure Java code, but does make some sense with JNI. There are probably other cases where it would make sense to take advantage of the extra knowledge that the API specifies.
新闻热点
疑难解答