【Effective Java 11】覆盖 equals 时总要覆盖 hashCode
1. hashCode 的基本约定
每一个覆盖了 equals 方法的类中,都必须覆盖 hashCode 方法。如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括 HashMap
和 HashSet
。下面是约定内容:
- 在同一个应用程序中,对于某一个对象,只要对象的 equals 方法的比较操作所用到的信息没有修改,那么对一个对象的多次调用,hashCode 方法都必须始终返回一个值。
- 如果两个对象根据
equals(Object)
相等,则 hashCode 方法产生的结果也必须相等。 - 如果两个对象根据
equals(Object)
方法比较是不相等的,那么调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果(有可能产生 Hash 碰撞),但是我们应该尽可能做到这点,这影响到该类型的对象在散列表中的效率。
2. 如何编写 hashCode 散列函数
一个好的散列函数通常倾向于 “为不相等的对象生成不相等的散列码”。理想情况下,应该把集合不相等的实例均匀地分布到所有可能的 int 值上。想要完全达到这种理想情况是十分困难的。幸运的是,相对接近这种理想情形并不困难。下面给出一种简单的解决方法:
- 声明一个
int
变量并命名result
,将它初始化为对象中第一个关键域的散列码c
,如步骤2.1 中计算所示(如第 10 条所述,关键域是指影响equals
函数比较的域) - 对象中剩下的每一个关键域
f
都完成以下步骤- 为该域计算
int
类型的散列码- 如果该领域是基本类型,则计算
Type.hashCode(f)
。如:Integer.hashCode(f)
- 如果该领域是一个对象引用,并且该类的
equals
方法通过递归地调用equals
的方式来比较这个域,则同样为这个域递归地调用 hashCode。如果需要更加复杂的比较,则为这个域计算一个 “范式” ,然后针对这个范式调用 hashCode。如果这个域的值为null
,则返回 0 (或者其他某个常数,但通常是0) - 如果该域是一个数组,则要把每一个元素当作单独的域来处理。对于中间的每一个元素都使用 2.2 的步骤处理。
- 如果该领域是基本类型,则计算
- 按照
result = 31 * result + c
的方式将 2.1 计算的散列码进行合并 - 返回
result
- 为该域计算
注意参与 hashCode 计算的域必须是 equals 关注的域。
3. 编写 hashCode 散列函数时的一些技巧
- Object 对象具有一个名为
hash
的静态方法,它接受任意数量的对象,返回他们组合的一个散列码。Object.hash(Object ...)
。但由于涉及到数组创建,以及基本类型的自动装箱,其性能较低。 - 对于不可变的类,可以在其每一个对象创建的时候就将 hashCode 计算完成并保存在对象,提升效率。