《Effective Java》之覆盖equals()时总要覆盖hashCode()

1.为什么覆盖equals()时总要覆盖hashCode()?
如果不这样做的话,就会违反了Object.hashCode()的通用约定。
通用约定如下:

  • 只要对象的equals()方法的比较操作所用到的信息没有被修改,那么多洗调用hashCode()方法都必须返回同一个整数。
  • 如果两个对象equals()判断相等,那么其hashCode()返回值也相等。
  • 如果两个对象hashCode()返回值相等,那么equals()判断不一定相等(尽可能的满足不同对象hashCode不一致,有可能提高散列表的性能)。

2.覆盖equals()而未覆盖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) {
        this.areaCode=(short)areaCode;
        this.prefix=(short)prefix;
        this.lineNumber=(short)lineNumber;
    }
    @Override//实现对象的逻辑相等
    public boolean equals(Object obj) {
        if(obj==this)   return true;
        if(!(obj instanceof PhoneNumber))
            return false;
        PhoneNumber pn=(PhoneNumber)obj;
        return pn.areaCode ==areaCode
                &&pn.prefix == prefix
                &&pn.lineNumber == lineNumber;
    }
    /*这里将自己实现的hashCode注释,测试的时候是没有这个方法的
    @Override //正确的hashCode()方法重写
    public int hashCode() {
        int result=17;
        result =31*result + areaCode;
        result =31*result + prefix;
        result =31*result + lineNumber;
        return result;
    }
    */
}

测试代码如下:

import hashCode.PhoneNumber;
//如果你不明白哈希表的工作原理,请自行百度
public class HashCodeMain {
    public static void main(String[] args) {
        Map<PhoneNumber, String> map=new HashMap<PhoneNumber, String>();
        PhoneNumber pn=new PhoneNumber(707, 867, 5309);
        map.put(pn, "Jenny");
        map.get(new PhoneNumber(707, 867, 5309));//null
        map.get(pn);//Jenny

    }   
}

上面的结果违反了第二条约定:相等的对象必须具有相等的散列值;此处的相等是根据equals()方法的逻辑相等,而默认的散列值却被实现成了与对象地址相关。可见equals()方法的逻辑相等与hashCode()方法的地址相等发生了冲突。
如果我们取消hashCode()方法的注释,那么这个测试代码的结果就会变成两个Jenny,而这才是我们想要的结果。

3.如何正确的覆盖hashCode()?

  • 把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
  • 对于对象中每个关键域f(指equals()方法中涉及的每个域),完成以下步骤:

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

1.如果该域是boolean,则计算(f ? 1:0)。
2.如果该域是byte,char,short,或int类型,则计算(int)f。
3.如果该域是long类型,则计算(int)(f^(f>>>32))。
4.如果该域是float类型,则计算Float.floatToBits(f)。
5.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤a.3,为得到的long类型计算散列值。
6.如果该域是一个对象引用,并且该类的equals()方法通过递归的调用equals()的方式来比较这个域,则同样为这个域递归地调用hashCode();如果这个域的值为null,则返回0。
7.如果该域是一个数组,则要把每一个元素当做单独的域来处理。可以递归地引用上述规则。如果数组中的每个元素都很重要,可以使用JDK1.5增加的方法Arrays.hashCode()方法。

  `public static int hashCode(long a[]) {
    if (a == null)
        return 0;    
    int result = 1;
    for (long element : a) {
        int elementHash = (int)(element ^ (element >>> 32));
        result = 31 * result + elementHash;
    }

    return result;
}`

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

  • 返回result(如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码作为属性存在对象内部,而不是每次请求的时候都重新计算散列码)。

参考资料:
《Effective Java》

posted @ 2018-04-09 17:34  李子君啊  阅读(93)  评论(0编辑  收藏  举报