如果equals方法被覆盖,hashCode方法也必须被覆盖
Java语言规范中为Object.hashCode方法制定了须遵守的合约:
1.m88体育 在程序的同一次运行(execution)中,给定某个对象,如果此对象用于计算equals返回值的信息没有改变,那么多次执行其hashCode方法,都必须返回同样的哈希值。当然,不同的运行之间,哈希值可以不同。
2.如果两个对象,依据equals方法的判定,是相等的,那么他们的哈希值必须相同。
3.m88体育 如果两个对象,依据equals方法的判定,是不相等的,那么他们的哈希值不一定不等。如果此情况下可以保证两哈希值不等,那么所有依赖哈希值的数据结构(HashMap,HashSet和Hashtable)的性能将被优化。
如果上述合约被破坏,在依赖哈希值的数据结构中,程序的行为将会出错。
以上三条合约中,最容易被破坏的是第二条:相等的对象必须有相同的哈希值。比如下面的例子:
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! ... // Remainder omitted }
这段代码覆盖了equals方法,而未覆盖hashCode方法,所以它将继承Object类中默认的hashCode:由对象的内存地址计算而来的一个整数。显然,对于由equals方法(已覆盖)判定相等的两个对象,hashCode很难相等,这将严重破坏Hash类数据结构的行为。比如:
Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); String nameOfJenny = m.get(new PhoneNumber(707, 867, 5309));
nameOfJenny将得到一个空值,而非预期的"Jenny"。这是因为jvm为HashMap做了优化:比较被搜索的key和表中存储的key时,如果二者哈希值不等,则直接判定不吻合,不会再用equals方法对二者进行比较。解决方法就是为上面的类覆盖hashCode方法:
@Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; return result; }
推荐的一套计算哈希值的方法:
第一步:选定一个正整数作为初始值
第二步:为每个在equals方法中参与计算的属性f,定义一个hash:
a.
i. f类型为boolean,计算(f?1:0)
ii. 类型为byte,char,short或者int,计算(int)f
iii. 类型为long,计算(int)(f^(f>>>32))
iv. 类型为float,计算Float.floatToIntBits(f)
v. 类型为double,计算Double.doubleToLongBits(f),然后继以上面的步骤iii
vi. f是对象,而且在equals方法中递归调用了该属性的equals方法,那么调用此属性的hashCode方法,以其结果作为此属性的哈希值
vii. f是数组,将其每个元素视作单独的属性分别计算,最后用下面的步骤b计算数组属性的hash。如果数组中每个元素都参与equals计算,还可以用Arrays.hashCode方法(Java 1.5以上版本)。
b.
将初始值和a步骤中计算出的哈希值用下面的算法合并:
result = 31 * result + c;
第三步:返回result
第四部:再次检验合约第二条,看equals判定相等的对象是否有相同的hash。并以unit test来检验。
写hashCode方法时,冗余属性(值可以由其他属性计算出来的属性)可以被忽略。如果hashCode方法开销巨大,可以考虑将哈希结果缓存起来,并采用lazy方式初始化:
// Lazily initialized, cached 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; }