在每个类中,重写equals方法的时候,一定要重写hashcode方法
以前在刚接触Java的hashMap的时候就知道了这个约定,也大概知道为什么。只是没有详细的整理梳理,现在看看《Effective Java》的说法:
- 当一个程序在执行过程中,如果在equals方法比较中没有修改任何信息,在一个对象上重复调用hashCode方法时,它必须返回相同的值。
- 如果两个对象根据equals(Object)方法比较是相等的,那么在两个对象上调用hashCode就必须产生结果相同的整数。
- 如果两个对象根据equals(Object)方法并不相等,则不要求在每个对象上调用hashCode都必须产生不同的结果,但是,程序员应该意思到,为不相等的对象生成不同的结果可能会提高散列表的性能。
下面分析一下上面的条款:
**首先**,hashcode是针对于散列表(hash tables)的,如果这个包装类能够确定在任何时候都不会在hash tables里面使用,可以不重写这个方法,但是这样做就限制了用户使用这个类的场景,这对于程序来说,是不符合规范的。
**其次**,我们分析下在散列表(hash tables)里面的情况:
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k))))
return e;
```
以上代码是来自HashMap.containsKey.getNode()中的一部分。
可以看到HashMap在判断一个对象是否被包含在容器里面的方法是先判断这个对象的HashCode是否相等,若HashCode相等,再使用equals是否相等。
再次,为什么需要用equals?这是因为hashCode的不可靠性和equals相对低效率性决定的,hashCode方法性能很高,使用hashcode索引一个对象时间复杂度为O(1),但是hashCode存在hash冲突,可能存在hashcode相同而对象不同的情况。因此使用equals做第二次验证。
生成正确的hashcode的方法:
- 声明一个int类型的变量result,并将其初始化为对象中的第一个影响对象逻辑相等的属性
c
的hash值。 - 对于对象中剩余的对对象逻辑相等有关的属性
f
,执行以下操作:- 比较属性
f
与属性c
的int 类型的哈希值- 如果这个属性是基本类型的,使用Type.hashCode(f)方法计算。
Type
为基本类型的对应的包装类 - 如果这个属性是一个对象的引用,并且这个类的equals方法通过递归调用equals来比较属性那么同样可以递归调用hashCode方法。如果需要复杂的比较,则计算此字段的标砖范式(canonical representation),并在范式上调用hashCode,如果该字段为空,则使用0(其他常数也行)表示。
- 如果这个属性是基本类型的,使用Type.hashCode(f)方法计算。
- 将步骤2.i中属性
c
计算出哈希值并合并为如下结果:result=31*result+c
3.返回result值
- 比较属性
步骤2.ii之所以使用31,是因为它具有两个特性,基数和素数。使用奇数是因为防止在相乘过程中乘法溢出导致信息丢失;素数是因为习惯上这样做。并且31*i
可以替换为位移和减法,这样性能更高:31*i=(i<<5)-i
- 高效的hashCode()方法
- 为了解决哈希冲突带来的碰撞,最好的方法可以参考com.google.common.hash包
- 普通生产环境使用2中所描述的方法即可。
- 对性能没有要求的时候,可以使用
Object
的静态方法:Object.hash()
获取hash值,但是这个里面的具体实现包括:创建数组以传递可变数量的参数,对基本类型进行装箱和取消装箱,性能比较低。
4.如果类是不可变的,并且计算hash值的代价比较大,可以考虑在对象中缓存哈希值。缓存可以分两种:懒加载和首次创建实例的时候加载。(首次调用hashCode再初始化会有线程安全的问题)