【Effective Java】Ch3_Methods:Item9_重写equals时总要重写hashCode()

来源:互联网 发布:杰奇网络账号 编辑:程序博客网 时间:2024/06/04 18:09

        一个常见的bug原因是没有覆盖hashCode方法。在每个覆盖了equals的类中,都必须覆盖hashCode。如果不这样,则会导致违反Object.hashCode()的通用约定,导致在与所有基于哈希码的集合无法一起正常工作,包括HashMap、HashSet、Hashtable。

        如下是Object规范中的通用约定:

  • 在程序的一次执行中只要equals方法所用到的信息没有改变,则多次调用同一个对象的hashCode(),都能始终返回相同的整数。在同一程序的多次执行中,每次的返回结果可以不同。
  • 如果equals方法判定两个对象相同,则这两个对象的hashCode必须产生相同的整数。
  • 如果equals方法判定两个对象不同,则这两个对象的hashCode可以不用;然而,程序员必须要清楚,给不相等的对象生成不同的hashCode可以提供哈希表的性能。

        关键的约定是第二条:相等的对象必须有相同的hashCode。如果未覆盖hashCode,则两个相等对象返回的哈希码是Object.hashCode()产生的两个随机数。

        【例】下面这个PhoneNumber类,其equals方法是根据Item8的诀窍构造出来的:

public final class PhoneNumber {  private final short areaCode;  private final short prefix;  private final short lineNumber;  public PhoneNumber(int areaCode, int prefix, int lineNumber) {    rangeCheck(areaCode, 999, "area code");    rangeCheck(prefix, 999, "prefix");    rangeCheck(lineNumber, 9999, "line number");    this.areaCode = (short) areaCode;    this.prefix = (short) prefix;    this.lineNumber = (short) lineNumber;  }  private static void rangeCheck(int arg, int max, String name) {    if (arg < 0 || arg > max)      throw new IllegalArgumentException(name +": " + arg);  }  @Override   public boolean equals(Object o) {    if (o == this)        return true;    if (!(o instanceof PhoneNumber))        return false;    PhoneNumber pn = (PhoneNumber)o;    return pn.lineNumber == lineNumber        && pn.prefix == prefix        && pn.areaCode == areaCode;  }  // Broken - no hashCode method!  ... // Remainder omitted}

        假设你打算在HashMap中使用这个类:

Map<PhoneNumber, String> m       = HashMap<PhoneNumber, String>();m.put(new PhoneNumber(707, 867, 5309), "Jenny");

        这时候你可能期望m.get(new PhoneNumber(707, 867, 5309))返回"Jenny",可它实际上却返回null。本例中两个PhoneNumber对象是equals的,但hashCode不同;导致在put的时候将对象A放到一个哈希桶A中,但get的时候却根据对象B的哈希码 到哈希桶B中却查找对象。

        即使碰巧对象AB都指向相同的哈希桶,get方法也会返回null;因为HashMap有一项优化,它会将每个项关联的哈希码缓存起来,当哈希码不同时,就不再去检查对象等同性了。


        修正这个问题非常简单,只要给PhoneNumber类提供一个合适的hashCode方法即可。那么hashCode()应该怎么写呢?编写一个合法但是不好用的hashCode是没有价值的,【例】如下hashCode()是合法的,但永远不应该这么写:

@Overridepublic int hashCode() {    return 42;}

        它是合法的,因为保证了相等的对象拥有相同的哈希码。但他也是很恶劣的,因为所有对象都拥有相同的哈希码。因此,每个对象都被放到相同的哈希桶中,使得哈希表(hash table)退化为链表(linked list)。本来应该线性时间运行的程序变成平方时间运行了,对于大型的哈希表,这会关系到能否正常工作。


        一个好的哈希函数会为不等的对象产生不等的哈希码。理想情况下,哈希函数能够将不相等的实例均匀分不到所有可能的哈希值上。达到这个理想情况是很难的。幸运的是,达到近似理想情况并不十分困难,下面 是简单的秘诀:

1、用一个int类型的变量result 保存一个非零的常数,例如17。

2、针对对象中的每个关键字段f (即equals方法中涉及的每个字段),执行以下操作:

    a. 计算该字段的哈希码:

  • 如果字段是boolean类型,计算 (f  ? 1 : 0)
  • 如果字段是byte, char, short, int,计算 (int) f
  • 如果字段是long,计算 (int) ( f ^ (f >>> 32))
  • 如果字段是float,计算Float.floatToIntBits(f)
  • 如果字段是double,计算Double.doubleToLongBits(f),然后对返回的long值进行上一步操作
  • 如果字段是对象引用,并且该类的equals通过递归调用该对象引用的equals来比较这个字段,则递归调用该字段的hashCode;如果需要更复杂的比较,则为该字段计算一个范式(canonical representation),在该范式上调用hashCode;如果该字段为null,则返回0(也可以返回其他常数,但一般用0)
  • 如果字段是数组,则将其每个元素当成一个单独字段来处理。如果数组中的每个元素都很重要,可以用Arrays.hashCode来处理。

    b. 将Step2.a中得到的值c,合并到result中

        result = 31 * result + c;

3、返回result

4、当你写完hashCode方法后,问问自己相同的实例是否返回相同的哈希码。编写单元测试来验证你的推论。



        在哈希码的计算过程中,你可以排除掉冗余字段。换言之,如果一个字段的值 可以通过hashCode计算过程中的其他字段计算出来,则可以忽略掉这个字段。你必须排除掉在equals比较中没有用到的字段,否则就有可能违反hashCode约定的第二条。

        在Step1中用了一个非零的初始值,所以Step2.a中计算得到哈希码为0的那些初始字段会影响到哈希值。如果Step1中的初始值为0,则最终的哈希值不会被这些初始字段影响,这样会增加冲突。17是任选的。

        Step2.b中的乘积使得计算结果依赖于字段顺序,如果类包含多个相似的字段,这样就会产生更好的哈希函数。【例】如果String类的hash函数忽略了乘积,则所有字符都具有相同的哈希码。

         其中乘数选择31,是因为它是一个奇素数。如果选用偶数,并且乘法溢出,则信息会丢失,因为与2相乘等价于移位操作。使用素数的好处并不明显,不过习惯上这么用。31的一个很好的特性是,乘积可以用移位和减法来替代,以达到更好的性能:

31 * i == (i << 5) - i
        现代VM会自动进行这种优化。


        我们将上述诀窍应用于PhoneNumber类,它有三个重要字段,都是short类型:

@Overridepublic int hashCode() {    int result = 17;    result = 31 * result + areaCode;    result = 31 * result + prefix;    result = 31 * result + lineNumver;    return result;}

        如果想哈希函数达到艺术级别,最好留给科学家去研究……

        不要试图在哈希码计算过程中排除掉任何重要字段以便提高性能。虽然这样哈希函数可能跑得更快,但是由于其质量不好,可能会降低哈希表的性能,以致哈希表慢到无法使用。特别是在实践中,哈希函数会面对大量的实例集合,这些实例恰恰在被你忽略的字段上有很大差异。如果是这样,那么哈希函数将把所有实例映射到少数几个哈希码上,以致基于哈希的集合显示出平方级的性能指标。

        这可不仅仅是理论问题,JDK1.2之前版本中String类的哈希函数最多只检查16个字符,从第一个字符开始在整个字符串中均匀选取。对于像URL这种层级结构的字符串集合来说,这种哈希函数正好体现了这种病态行为。


        Java平台包中的许多类,例如String、Integer、Date,都指定将其hashCode()方法返回的确切值作为实例值得一个函数。这通常不是个好主意,因为这限制了在后续版本中对哈希函数做改进的能力。而如果不指定hashCode()方法的细节,则如果以后发现了缺陷,或者找到了更好的哈希函数,那么就可以在后续版本中更改哈希函数,并确信没有客户端依赖于哈希函数返回的确切值。