Effective Java(3rd)-Item11 重写equals时始终重写hashCode

  你必须在每个类中重写hashCode,只要你重写了equals。如果你不这么做,你的类将违反了hashCode的一般约定,这将阻止它在HashMap和hashSet这样的集合中正常运作。如下是改编自对象规范的约定:

  • 当在应用程序执行期间对对象重复调用 hashCode 方法时,它必须一致地返回相同的值,前提是不对 equals 比较中使用的信息进行修改。在应用程序不同的执行期间,这个值不需要一直保持一致。
  • 如果两个对象根据通过equals(Object)方法相等,则在这两个对象调用hashCode时,必须生成相同的整数结果。
  • 如果两个对象根据equals(Object)方法得出不相等,则不需要在每个对象上调用hashCode必须不相同。然而,程序员应该意识到,为不相同对象生成不相同的结果可能会提高hash表的性能。
      当你不能重写hashCode时,就违反了第二个关键条约:相等的对象必须要有相等的hashCode。两个不同的实例可能因为类的equals方法在逻辑上相等,但是对于Object的hashCode来说,它们只是两个没有多少共同点的对象。因此,Object的hashCode方法返回了两个看似随机的数字,而不是条约要求的两个相等的数字。
      例如,假设你尝试使用item10中的PhoneNumber类的实例作为HashMap的键:
    image.png

  此时,你可能期望m.get(new PhoneNumber(707, 867, 5309)) 返回“Jenny”,但是相反,它返回了null。注意到涉及到两个PhoneNumber的实例:一个用于插入HashMap,另一个相等的实例用于(尝试)检索。PhoneNumber类未能重写hashCode造成了两个相等实例具有不相等的hashCode,违反了hashCode约定。因此,get方法可能与put方法存储的在不同的hash桶中查询phoneNumber。即使两个实例碰巧hash到了同一个桶,get方法机会肯定会返回null,因为HashMap有一个优化,缓存了每个条目相关的hashCode,如果hashCode没有,则不需要检查对象是否相等。
  修复这个问题就像为PhoneNumber编写一个合适的hashCode意义简单。那么hashCode应该长什么样呢?很容易写好一个糟糕的例子。例如,这个代码总是合法的,但是永远不该被使用:


image.png

  这个合法的,因为它确保了相等的对象拥有相同的hashCode。这是糟糕的,因为它确保了每个对象都有了相同的hashCode。因此,每个对象散列到了相同的桶中,哈希表退化到了linked list。程序本应以线性时间运行,但是以二次方时间运行了。对于大型哈希表,这是工作与不工作的区别。
  一个好的hash函数往往为不相等的实例生成不相等的hashCode。这恰恰是hashCode约定第三条的意义。理想情况下, hash 函数应该在所有 int 值之间均匀分布所有不相等实例的合理集合。实现这个想法可能非常困难。幸运的是,实现一个类似的集合并不太难。如下是一个简单的菜谱:
1.声明一个int变量叫做result,初始化它为hashCode c作为你的对象的第一个重要字段,如步骤2.a计算的那样(回顾item10,重要字段影响euqals的比较)。
2.对于你的对象中每个剩下的重要字段f,执行如下操作:
  a.计算字段的int hashCode c:
    i.如果字段是基本类型,计算Type.hashCode(f),Type是与f类型对于的包装类型。
    ii.如果字段是一个对象引用,并且这个类的equals方法通过递归调用equals来比较该字段,则在该字段上递归调用hashCode。如果需要更复杂的比较,则为这个字段计算“规范表示”并在规范表示上调用hashCode。如果字段的值是null,使用0(或其他常量,但0是传统)。
    iii.如果字段是数组,视其每个重要元素都为独立的字段。也就是说,通过递归地调用准备些规则为每个重要的元素计算hashCode,组合每个步骤的2.b。如果数组没有重要元素,使用一个常量,最好不要是0.如果所有元素都是重要的,使用Arrays.hashCode.
  b.联合在step2.a计算的hashCode c组合到result中,如下所示:
     result = 31 * result + c;
3.返回result。
  当你完成编写hashCode方法时,问问你自己是否相等的实例有着相等的hashCode.编写单元测试来证实你的直觉(除非你使用AutoValue来生成你的equals和hashCode方法,在这种情况下你可以安全地省略这些测试)。如果相等实例有着不相等的hashCode,指出原因并修复这些问题。
  你可以从hashCode计算中排除派生字段。换句话说,你可以忽略任何可以从计算中包含的字段计算其值的字段。你必须排除不在equals比较使用的任何字段。否则你就有违反hashCode第二条约定的风险。
  步骤2.b的乘法使得结果取决于字段的顺序,如果类有多个相似字段,就能产生一个更好的哈希函数。例如,如果在String哈希函数中忽略了乘法,所有“anagrams”(译者注:
比如 “abc” "acb" "cba" 如果没有乘法,这三个String的hashCode必定一致)具有相同的hashCode。选择31是因为它是一个奇数质数。如果它是偶数且乘法溢出,信息将被丢失,因为乘2等同于移位。使用素数的优势不太明确,但是它是传统的。31的一个很好的特性是乘法可以被以为和减法代替,以便在一些架构上获得更好的性能: 31 * i == (i << 5) - i。现在VM自动执行这类优化。
  让我们将这些配方应用在PhoneNumber类中:

Effective Java(3rd)-Item11 重写equals时始终重写hashCode_第1张图片
image.png

  因为这个方法返回了一个简单确定性计算的结果,其输入仅为一个PhoneNumber实例中的三个重要字段,显然相同PhoneNumber有着相同的hashCode。事实上,这个方法对于PhoneNumber来说是一个非常好的hashCode实现,与Java平台库中的方法相同。它很简单,速度相当快,可以合理地分散不相等的phone numbers到不同的哈希桶中。
  虽然这个项目中的配方产生了相当好的哈希函数,但是它们不是最先进的。它们的质量与Java平台库的值类型中的哈希函数相当,并且适用于大多数用途。如果你真正需要哈希函数不太可能产生冲突,查看 Guava’s com.google.common.hash.Hashing [Guava]。
  Objects类有一个静态方法,接受一个任意数量的对象并且返回它们的hashCode。这个方法的名字叫做”hash“。允许你编写单行hashCode方法,其质量与根据本条目中的配方编写的方法相当。不幸的是,它们运行地更慢因为它们需要数组创建来传递可变数量地参数,以及装箱和取消装箱,如果参数有原始类型的话。建议仅在性能不那么重要的情况下使用这个形式的哈希函数。这是使用这种技术编写的PhoneNumber的哈希函数:


image.png

  如果类是不可变的并且计算hashCode的开销是巨大的,你可能需要考虑在对象内缓存hashCode而不是在每次请求时重新计算。如果你认为这个类型的大多数对象将用作hash key,你应该在实例被创建的时候计算hashCode。否则,你可能在你第一次调用hashCode时选择懒初始化hashCode。需要注意确保存在懒初始化字段时保证线程安全 (item83) 。我们的PhoneNumber类不值得这么处理,只是想你展示它是怎么完成的,如你所见。注意hashCode初始化值不该是常用实例
的哈希码(本例中是0):

Effective Java(3rd)-Item11 重写equals时始终重写hashCode_第2张图片
image.png

  不要试图在哈希码计算时排除重要字段来提高性能。虽然生成的哈希函数可能会运行更快,但是它的低质量可能回事哈希表的性能降低到不能使用的程度。特别地,哈希函数可以面对大量实例的集合,主要区别在于你的选择性忽略的区域。如果这种情况发生了,哈希函数将映射所有的实例到一小部分hashCode,本该在线性时间运行的程序将不得不使用n^2的时间。
  这不仅仅只是一个理论问题。在Java2之前,String的哈希函数在整个字符串中均匀分布最多16个字符,从第一个字符开始。对于大型分层名称集合,比如URL,这个方法完全显示前面描述的病态行为。
  不要为hashCode返回的值提供详细的规范,因为这样客户端就不能合理地依赖它;这可以是你灵活地改变它。许多在Java库中的类,比如String和Integer,将hashCode方法返回的确切值指定为实例值得函数。这不是一个好主意,但是这是一个我们不得不忍受的错误:它阻碍了在未来版本中改进哈希函数的能力。如果未指定详细信息并且在散列函数中发现缺陷或发现更好的散列函数,你就可以在后续版本中改变它。
  总之,每次覆写equals方法时你必须覆写hashCode,否则你的程序将无法运行正确。你的hashCode方法必须遵守Object中指定的常规规则,并且必须合理地将不相等的hashCode分配给不相等的实例。只要使用51页的配方,这就很容易实现。在item10提到的AutValue框架,提供了自动编写equals和hashCode方法的优秀替代方案,IDE也提供了一些功能。
本文写于2019.3.2,历时5天

你可能感兴趣的:(Effective Java(3rd)-Item11 重写equals时始终重写hashCode)