《Effective Java》 读书笔记(十一) 重写equals方法时也要重写hashCode方法

在每个类中,重写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的方法:

  1. 声明一个int类型的变量result,并将其初始化为对象中的第一个影响对象逻辑相等的属性c的hash值。
  2. 对于对象中剩余的对对象逻辑相等有关的属性f,执行以下操作:
    1. 比较属性f与属性c的int 类型的哈希值
      • 如果这个属性是基本类型的,使用Type.hashCode(f)方法计算。Type为基本类型的对应的包装类
      • 如果这个属性是一个对象的引用,并且这个类的equals方法通过递归调用equals来比较属性那么同样可以递归调用hashCode方法。如果需要复杂的比较,则计算此字段的标砖范式(canonical representation),并在范式上调用hashCode,如果该字段为空,则使用0(其他常数也行)表示。
    2. 将步骤2.i中属性c计算出哈希值并合并为如下结果:result=31*result+c
      3.返回result值

步骤2.ii之所以使用31,是因为它具有两个特性,基数和素数。使用奇数是因为防止在相乘过程中乘法溢出导致信息丢失;素数是因为习惯上这样做。并且31*i可以替换为位移和减法,这样性能更高:31*i=(i<<5)-i

  1. 高效的hashCode()方法
  • 为了解决哈希冲突带来的碰撞,最好的方法可以参考com.google.common.hash
  • 普通生产环境使用2中所描述的方法即可。
  • 对性能没有要求的时候,可以使用Object的静态方法:Object.hash()获取hash值,但是这个里面的具体实现包括:创建数组以传递可变数量的参数,对基本类型进行装箱和取消装箱,性能比较低。

4.如果类是不可变的,并且计算hash值的代价比较大,可以考虑在对象中缓存哈希值。缓存可以分两种:懒加载和首次创建实例的时候加载。(首次调用hashCode再初始化会有线程安全的问题)

猜你喜欢

转载自www.cnblogs.com/dengchengchao/p/9066221.html
今日推荐