重写hashCode的规范
每个重写equals方法的类中,也必须重写hashCode方法。
如果不覆盖hashCode,会导致无法结合基于散列的集合正常工作,例如HashMap、HashSet和Hashtable等等,换句话说,实现了对的hashCode,就可以拿对象的实例作为Hash集合的Key,下面是重写hashCode的规范:
- 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致;
- 如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任何一个对象的hashCode方法都必须产生同样的整数结果;
- 如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高山列表(hash table)的性能;
相等的对象必须具有相等的散列码(hash code)。
两个不同的实例在逻辑上有可能是相等的(equals),但是hashCode方法返回的应该是两个不同的随机整数,考虑下面这个PhoneNumber类,在企图与HashMap一起使用时,将失败:
package test.ch02;
public class PhoneNumber {
private final int areaCode;
private final int prefix;
private final int lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
public int getAreaCode() {
return areaCode;
}
public int getPrefix() {
return prefix;
}
public int getLineNumber() {
return lineNumber;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
}
}
package test.ch02;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
String s = m.get(new PhoneNumber(707, 867, 5309));
System.out.println(s); // null
}
}
由于PhoneNumber没有重写hashCode方法,从而导致两个相等的实例具有不相等的散列码。
修正这个问题很简单,只需要为PhoneNumber提供一个适当的hashCode即可。
如何重写规范的hashCode方法
一个好的hashCode方法倾向于“为不相等的对象产生不相等的散列码”。
理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上,可以采用如下做法:
- 把某个非零的常数值,比如17,保存在一个result的int类型变量中;
- 对于对象中每个关键域f,完成以下步骤:
- 为该域计算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),在根据long计算;
- 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode;
- 如果该域是一个数组,则要把每一个元素当作单独的域来处理,也可以使用Arrays.hashCode方法;
- 按照result = 31 * result + c来计算散列码;
为PhoneNumber重写一个hashCode:
package test.ch02;
public class PhoneNumber {
private final int areaCode;
private final int prefix;
private final int lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
public int getAreaCode() {
return areaCode;
}
public int getPrefix() {
return prefix;
}
public int getLineNumber() {
return lineNumber;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
}
package test.ch02;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
String s = m.get(new PhoneNumber(707, 867, 5309));
System.out.println(s); // Jenny
}
}
优化hashCode方法
- 在散列码的计算过程中,可以把冗余域(redundant field)排除在外。
- 为什么选择31做散列码,是因为它是一个奇素数。31有个很好的特性,即用位移法和减法来代替乘法,可以得到更好的性能: 31 * i = (i<<5)-i。现代的VM可以自动完成这种优化;
- 如果一个类是不可变的,并且计算散列码的开销很大,就应该考虑把散列码缓存在对象内部。或者考虑延迟初始化散列码,在第一次调用hashCode时缓存到内部;
- 不要视图从散列码计算中排除掉一个对象的关键部分来提高性能;