ITEM 11:重写equals时也要复写hashcode

2019-05-02  本文已影响0人  rabbittttt

ITEM 11:ALWAYS OVERRIDE HASHCODE WHEN YOU OVERRIDE EQUALS
  必须在每个重写了 equals 方法的类中重写 hashCode 方法,如果不这样做,您的类将违反 hashCode 的通用契约,这可能导致它在 HashMap 和HashSet 这类集合中运行异常。根据 Object 规范,下面是具体的条款:

// The worst possible legal hashCode implementation - never use!
@Override public int hashCode() { return 42; }

  这是合法的,因为它确保相等的对象具有相同的哈希码。但它很糟糕,因为它让所有对象都有相同的哈希码。因此,每个对象都散列到同一个桶中,散列表退化为链表。程序应该在 O(n) 时间运行,而不是在 O(n2) 运行。对于大型哈希表,可能导致程序无法工作。
  一个好的哈希函数往往会为不相等的实例生成不同的哈希码。这正是 hashCode 契约的第三部分的含义。理想情况下,哈希函数应该在所有 int 值上均匀地分布任何合理的不相等实例集合。实现这一理想可能很困难。幸运的是,得到一个合理的近似并不难。这里有一个简单的方法:

// Typical hashCode method
@Override public int hashCode() {
  int result = Short.hashCode(areaCode);
  result = 31 * result + Short.hashCode(prefix); 
  result = 31 * result + Short.hashCode(lineNum); 
  return result;
}

  由于该方法返回一个简单确定性计算的结果,该计算的唯一输入是PhoneNumber实例中的三个重要字段。显然,相同的PhoneNumber实例具有相同的哈希码。实际上,这个方法是 PhoneNumber 的一个非常好的 hashCode 实现,可以与Java平台库中的实现相媲美。它很简单,速度也相当快,并且能够将不相等的电话号码分散到不同的散列桶中。
  虽然上面的例子是一个相当好的散列函数,但它们并不是最棒的。它们在质量上可以与Java平台库的值类型中的散列函数相媲美,并且对于大多数用途都是足够的。如果您确实需要不太可能产生冲突的散列函数,请参阅Guava的com.google.common.hash.Hashing [Guava]。
  Objects类有一个静态方法,它接受任意数量的对象并为它们返回一个散列代码。这个名为 hash 的方法允许您编写单行 hashCode 方法,其质量可与根据该项目中的配方编写的方法相媲美。不幸的是,它们运行得更慢,因为它们需要创建数组来传递可变数量的参数,如果其中任何参数是原始类型,则需要装箱和解箱。建议只在性能不重要的情况下使用这种类型的哈希函数。下面是一个使用这种技术编写的PhoneNumber 函数:

// One-line hashCode method - mediocre performance
@Override public int hashCode() {
  return Objects.hash(lineNum, prefix, areaCode);
}

  如果一个类是不可变的,并且计算散列代码的成本很高,那么您可以考虑将散列代码缓存到对象中,而不是每次请求时重新计算它。如果您认为这种类型的大多数对象都将用作散列键,那么您应该在创建实例时计算散列代码。否则,您可能选择在第一次调用散列代码时惰性地初始化散列代码。需要注意,确保类在存在延迟初始化的字段时仍然是线程安全的。我们的 PhoneNumber 类不需要这种处理,但只是向您展示它是如何完成的,在这里。注意,hashCode字段的初始值(在本例中为0)不应该是通常创建的实例的哈希码:

// hashCode method with lazily initialized cached hash code private int hashCode; 
// Automatically initialized to 0
@Override public int hashCode() { 
  int result = hashCode;
  if (result == 0) {
    result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix); 
    result = 31 * result + Short.hashCode(lineNum); 
    hashCode = result;
  }
  return result; 
}

  不要试图从哈希代码计算中排除重要字段来提高性能。虽然生成的哈希函数可能运行得更快,但其质量差可能会降低哈希表的性能,使其无法使用。特别是,哈希函数可能会遇到大量实例,这些实例主要在您选择忽略的区域中不同。如果发生这种情况,哈希函数将把所有这些实例映射到几个哈希代码,应该在线性时间内运行的程序将在二次时间内运行。
  这不仅仅是一个理论问题。在Java 2之前,字符串哈希函数最多使用16个字符,以第一个字符开始,在整个字符串中平均间隔。对于层次结构名称的大集合(如url),此函数完全显示了前面描述的病态行为。
  不要为 hashCode 返回的值提供详细的规范,因此客户端不能合理地依赖它;这使您可以灵活地更改它。Java库中的许多类,如 String 和 Integer,都指定它们的hashCode 方法返回的确切值作为实例值的函数。这不是一个好主意,而是我们不得不面对的一个错误:它阻碍了在未来版本中改进 hash 函数的能力。如果您没有指定细节,并且在散列函数中发现了一个缺陷,或者发现了一个更好的散列函数,您可以在后续版本中修改它。
  总之,每次重写 equals 时都必须重写 hashCode,否则程序将无法正确运行。您的hashCode 方法必须遵守对象中指定的通用契约,并且必须合理地为不相等的实例分配不相等的哈希码。使用本文提供的规范,这很容易实现,尽管有点乏味。正如item 10 中提到的,AutoValue 框架提供了一个很好的替代方法,可以替代手工编写 equals 和hashCode 方法,IDE也提供了一些功能。

上一篇下一篇

猜你喜欢

热点阅读