本文是《Effective Java》读书笔记第9条,其中内容可能会结合实际应用情况或参考其他资料进行补充或调整。
在每个覆盖了equals
方法的类中,一定也要覆盖hasCode
方法。否则会导致该类无法结合所有基于散列的集合(比如HashMap、HashSet、HashTable等)一起正常工作。
这一原则出自Java Object的规范(其实是第二条):
1. 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这一对象调用多次,hashCode
方法必须始终返回统一整数。而在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
2. 如果两个对象的equals
方法比较是相等的,那么两个对象的hashCode
方法应该返回同样的整数结果。而如果equals
方法比较结果是不相等的,那么两个对象的hashCode
方法不一定要返回不同的整数结果(应该注意的一点是,不相等的对象产生不同的整数结果,一定程度上可以提高散列表的性能)。
给一个简单的例子:
public class PhoneNumber {
private final int areaCode;
private final int prefix;
private final int lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix code");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = 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;
else if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.areaCode == this.areaCode && pn.prefix == this.prefix && pn.lineNumber == this.lineNumber;
}
// 这里没有覆盖hasCode方法
public static void main(String[] args) {
HashMap<PhoneNumber, String> map = new HashMap<>();
PhoneNumber pn1 = new PhoneNumber(123, 456, 7890);
PhoneNumber pn2 = new PhoneNumber(123, 456, 7890);
System.out.println(pn1.equals(pn2));
map.put(pn1, "jack");
System.out.println(map.get(pn2));
}
}
可以看到,pn1和pn2是equal的。本来期望map.get
返回结果是“jack”,结果返回的是null
。这是由于PhoneNumber
没有覆盖hashCode
方法,导致两个对象具有不同的散列码,因而对于HashMap对象来说,这是两个不同的key。
修正这一问题的方法就是提供一个适当的hashCode
方法。何为“适当”的hashCode
方法呢:
- 选择一个非零的常数值,比如19;
- 对于对象中的每个关键域f(就是
equals
方法中涉及的每个域),完成如下步骤:
- 为该域计算int类型的散列码c:
- 如果该域是boolean类型,则计算
(f ? 1 : 0)
; - 如果该域是
byte
、char
、short
或int
类型,则计算(int)f
; - 如果该域是
long
类型,则计算(int)(f ^ (f >>> 32))
; - 如果该域是
float
类型,则计算Float.floatToIntBits(f)
; - 如果该域是
double
类型,则计算Double.doubleToLongBits(f)
,然后在按照2.1.3计算; - 如果该域是一个对象引用,如果值为
null
,则返回0;否则返回该对象的hashCode
值; - 如果该域是一个数组,则可以利用
Arrays.hashCode
方法来处理。
- 如果该域是boolean类型,则计算
- 按照下边的公式计算result
result = 31 * result + c;
- 为该域计算int类型的散列码c:
- 返回result;
- 编写单元测试来验证。
其中,应该注意,如果一个域的值可以根据参与计算的其他域的值计算出来,那么这样的域应该排除在外。此外,之所以经常选择31来乘以result,是因为31是一个奇素数,它有个很好的特性就是可以利用移位和减法来代替乘法运算,从而得到更好的性能,比如 31*i=(i<<<5)-i
。
最终,刚才的例子中的hashCode可以这么写:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
当然,如果计算散列码的开销特别大,那么可以将散列码缓存在对象内部,而不是每次请求都计算散列码:
private volatile int hasCode;
@Override
public int hashCode() {
int result = hasCode;
if (result == 0) {
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}