首页 > 编程 > Java > 正文

Effect Java 第9条:覆盖equals时总要覆盖hashCode

2019-11-08 01:21:09
字体:
来源:转载
供稿:网友

1、什么是hashCode

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,所以hashCode可以表明一个对象的地址信息,如果两个对象的hashCode相等,那么它们的地址相同,也就是说它们是一歌对象。

2、为什么覆盖equals后必须覆盖hashCode

Object规范:

在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果

如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不一样的结果。但是,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

那么当我们覆盖了equals方法后,有可能出现两个不同实例的对象,但是它们因为覆盖了equals方法后,逻辑上相等。这时候会出现equas方法返回true,但是hashCode不相等的情况。例如:

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; } public static void main(String[] args) { Map m = new HashMap(); m.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(m.get(new PhoneNumber(707, 867, 5309))); }}

这时候,我们希望输出”Jenny”,但它实际却返回null。原因是没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。修正这个问题也很简单,只需要为该类提供想对应的hashCode方法即可。

3.怎么重写hashCode方法

1.把某个非零的常数值,如17,保存在一个名为result的int类型的变量中。(为了2.a中计算的散列值为0的初始域会影响到散列值)

2.对于对象中的每个关键域f,完成一下步骤:

 a.为该域计算int类型的散列码c

  i.如果该域是boolean,计算(f ? 1:0)

  ii.如果该域是byte、char、short或者int类型,则计算(int)f

  iii.如果该域是long,则计算(int)(f ^ (f >>> 32))

  iv.如果该域是float,则计算Float.floatToIntBits(f)

  v.如果该域是double,则计算Double.doubleToLongBits(f),然后

  vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个“范式”调用hashCode。如果域的值为null,则返回0(或其他某个常数,但通常为0)。

  vii.如果该域是一个数组,则要吧每一个元素当做单独的域来处理,也就是要递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据2.b把这些散列值组合起来。如果数组域中的每个元素都很重要,可以使用1.5中增加的其中一个Array.hashCode方法。

 b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:

  result = 31 * result + c。(选择31是因为它是一个奇素数,如果乘数是偶数,乘法溢出时会丢失信息,VM可以优化 31 * i == (i << 5) - i)

3.返回result。

编写完hashCode方法后,编写单元测试来验证相同的实例是否有相等的散列码。

把上面的解决方法应用到PhoneNumber类中:

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

现在使用之前的测试代码,发现能够返回Jenny了。

如果一个类是不可变的,并且计算散列码的开销很大,应该考虑把散列码缓存到对象内部而不是每次请求都重新计算散列码,如果这种类大多数对象会被用作散列键,应该在创建实例的时候计算散列码,否则可以选择延迟初始化散列码。

注意:不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。虽然这样做运行起来可能更快,但效果不见得好,在拥有大量实例的时候,忽略的域区别仍然非常大,但散列函数仍然把它们映射到同样的散列桶中,例如java 1.2之前实现的String散列函数至多检查16个字符,对于像URL这样的大型集合,散列函数表现出病态的行为(把第16个字符后相差非常大的URL映射到同样的散列桶中,使得碰撞率很高,性能降低)。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表