建议:覆盖equals时总要覆盖hashCode。

一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法,如果不这样的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

下面是约定的内容,摘自Object规范【JavaSE6】:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样地整数结果。
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hashtable)的性能。

注意:HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的灯通行。

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

1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中。

2、对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤:

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

i、    如果该域是boolean类型,则计算(f ? 1 : 0)。

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

iii、  如果该域是long类型,则计算(int)(f ^ (f >>> 32))。

iiii、 如果该域是float类型,则计算Float.floatToIntBits(f)。

V、  如果该域是double类型,则计算Double.doubleToLongBits(f)。然后按照步骤2.a.iii,为得到的long类型值计算散列值。

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

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

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

result = 31*result + c;

3、返回result。

4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。

在散列码的计算过程中,可以把冗余域(redundant field)排除在外。换句话说,如果一个域的值可以根据参与计算的其它域值计算出来,则可以把这样的域排除在外。必须排除equals比较计算中没有用到的任何域,否则很有可能违反hashCode约定的第二条。

上述步骤1用到了一个非零的初始值,因此步骤2.a中计算的散列值为0的那些初始域,会影响到散列值。如果步骤1的初始值为0,则整个散列值将不受这些初始域的影响,因为这些初始域会增加冲突的可能性。值17则是任意选的。

步骤2.b中的乘法部分使用散列值依赖于域的顺序,如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好地散列函数。例如,如果String散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。之所以选择31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算,使用素数的好处并不很明显,但是习惯上都是用素数来计算散列结果。31有个很好地特性,即用移位和减法替代乘法,可以得到更好地性能:31*i == (i << 5) - 5.现代的VM可以自动完成这种优化。

如果一个类时不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用作散列键(hash keys),就应该在创建实例的时候计算散列码。否则,可以选择“延迟初始化(lazily initialize)”散列码,一直到hashCode被调用的时候初始化。

不要试图从散列码计算中排除掉一个对象的关键部分来提高性能。虽然这样得到的散列函数运行起来可能更快,但是他的效果不见得会好,可能会导致散列表慢到根本无法使用。特别是在实践中,散列函数可能面临大量的实例,在你选择忽略的区域之中,这些实例仍然区别非常大。如果是这样,散列函数就会把所有这些实例映射到极少数的散列码上,基于散列的集合将会显示出平方级的性能指标。这不仅仅是理论问题。在Java1.2发行版本之前实现的String散列函数至多只检查16个字符,从第一个字符开始,在整个字符串中均匀选取。对于像URL这种层次状名字的大型集合,该散列函数正好表现出了这里所提到的病态行为。

代码示例:

 

public final class PhoneNumber {

private final short areaCode;

private final short prefix;

private final short lineNumber;

// Lazily initialized , cached hashCode

private volatile int hashCode; 

public PhoneNumber(int areaCode , int prefix , int lineNumber) {

rangeCheck(areaCode, 999 , "area code");

rangeCheck(prefix, 999 , "prefix");

rangeCheck(lineNumber, 999 , "lineNumebr");

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) 

throws new IllegalArgumentException(name + ": " + arg);

}

@Override

public boolean equals(Object o) {

if (o == this)

return true;

if(! ( o instanceof PhoneNumber ) )

reuturn false;

PhoneNumber pn = (PhoneNumber) o;

return pn.linerNumber == linNumber && pn.prefix == prefix && pn.areaCode == areaCode;

}


@Override

public int hashCode() {

int result = hashCode;

if (result == 0) {

result = 17;

result = 31 * result + areaCode;

result = 31 * result + prefix;

result = 31 * result + lineNumber;

hashCode = result;

}

return result;

}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值