【Effective Java 11】覆盖 equals 时总要覆盖 hashCode

1. hashCode 的基本约定

每一个覆盖了 equals 方法的类中,都必须覆盖 hashCode 方法。如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括 HashMapHashSet。下面是约定内容:

  • 在同一个应用程序中,对于某一个对象,只要对象的 equals 方法的比较操作所用到的信息没有修改,那么对一个对象的多次调用,hashCode 方法都必须始终返回一个值。
  • 如果两个对象根据 equals(Object)相等,则 hashCode 方法产生的结果也必须相等。
  • 如果两个对象根据 equals(Object) 方法比较是不相等的,那么调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果(有可能产生 Hash 碰撞),但是我们应该尽可能做到这点,这影响到该类型的对象在散列表中的效率。

2. 如何编写 hashCode 散列函数

一个好的散列函数通常倾向于 “为不相等的对象生成不相等的散列码”。理想情况下,应该把集合不相等的实例均匀地分布到所有可能的 int 值上。想要完全达到这种理想情况是十分困难的。幸运的是,相对接近这种理想情形并不困难。下面给出一种简单的解决方法:

  1. 声明一个 int 变量并命名 result,将它初始化为对象中第一个关键域的散列码 c,如步骤2.1 中计算所示(如第 10 条所述,关键域是指影响 equals 函数比较的域)
  2. 对象中剩下的每一个关键域 f 都完成以下步骤
    1. 为该域计算 int 类型的散列码
      1. 如果该领域是基本类型,则计算 Type.hashCode(f)。如:Integer.hashCode(f)
      2. 如果该领域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals 的方式来比较这个域,则同样为这个域递归地调用 hashCode。如果需要更加复杂的比较,则为这个域计算一个 “范式” ,然后针对这个范式调用 hashCode。如果这个域的值为 null ,则返回 0 (或者其他某个常数,但通常是0)
      3. 如果该域是一个数组,则要把每一个元素当作单独的域来处理。对于中间的每一个元素都使用 2.2 的步骤处理。
    2. 按照 result = 31 * result + c 的方式将 2.1 计算的散列码进行合并
    3. 返回 result

注意参与 hashCode 计算的域必须是 equals 关注的域。

3. 编写 hashCode 散列函数时的一些技巧

  • Object 对象具有一个名为 hash 的静态方法,它接受任意数量的对象,返回他们组合的一个散列码。Object.hash(Object ...)。但由于涉及到数组创建,以及基本类型的自动装箱,其性能较低。
  • 对于不可变的类,可以在其每一个对象创建的时候就将 hashCode 计算完成并保存在对象,提升效率。

posted on 2022-04-11 11:04  Silgm  阅读(65)  评论(0)    收藏  举报

导航