9. 【对于所有对象都通用的方法】重写equals方法时一定也要重写hashCode方法

本文是《Effective Java》读书笔记第9条,其中内容可能会结合实际应用情况或参考其他资料进行补充或调整。


在每个覆盖了equals方法的类中,一定也要覆盖hasCode方法。否则会导致该类无法结合所有基于散列的集合(比如HashMap、HashSet、HashTable等)一起正常工作。
这一原则出自Java Object的规范(其实是第二条):
1. 在应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这一对象调用多次,hashCode方法必须始终返回统一整数。而在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
2. 如果两个对象的equals方法比较是相等的,那么两个对象的hashCode方法应该返回同样的整数结果。而如果equals方法比较结果是不相等的,那么两个对象的hashCode方法不一定要返回不同的整数结果(应该注意的一点是,不相等的对象产生不同的整数结果,一定程度上可以提高散列表的性能)。
给一个简单的例子:

public class PhoneNumber {

    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix code");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = 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;
        else if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.areaCode == this.areaCode && pn.prefix == this.prefix && pn.lineNumber == this.lineNumber;
    }

    // 这里没有覆盖hasCode方法

    public static void main(String[] args) {
        HashMap<PhoneNumber, String> map = new HashMap<>();
        PhoneNumber pn1 = new PhoneNumber(123, 456, 7890);
        PhoneNumber pn2 = new PhoneNumber(123, 456, 7890);
        System.out.println(pn1.equals(pn2));
        map.put(pn1, "jack");
        System.out.println(map.get(pn2));
    }
}

可以看到,pn1和pn2是equal的。本来期望map.get返回结果是“jack”,结果返回的是null。这是由于PhoneNumber没有覆盖hashCode方法,导致两个对象具有不同的散列码,因而对于HashMap对象来说,这是两个不同的key。
修正这一问题的方法就是提供一个适当的hashCode方法。何为“适当”的hashCode方法呢:

  1. 选择一个非零的常数值,比如19;
  2. 对于对象中的每个关键域f(就是equals方法中涉及的每个域),完成如下步骤:
    1. 为该域计算int类型的散列码c:
      1. 如果该域是boolean类型,则计算(f ? 1 : 0)
      2. 如果该域是bytecharshortint类型,则计算(int)f
      3. 如果该域是long类型,则计算(int)(f ^ (f >>> 32))
      4. 如果该域是float类型,则计算Float.floatToIntBits(f)
      5. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后在按照2.1.3计算;
      6. 如果该域是一个对象引用,如果值为null,则返回0;否则返回该对象的hashCode值;
      7. 如果该域是一个数组,则可以利用Arrays.hashCode方法来处理。
    2. 按照下边的公式计算result result = 31 * result + c;
  3. 返回result;
  4. 编写单元测试来验证。

其中,应该注意,如果一个域的值可以根据参与计算的其他域的值计算出来,那么这样的域应该排除在外。此外,之所以经常选择31来乘以result,是因为31是一个奇素数,它有个很好的特性就是可以利用移位和减法来代替乘法运算,从而得到更好的性能,比如 31*i=(i<<<5)-i

最终,刚才的例子中的hashCode可以这么写:

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

当然,如果计算散列码的开销特别大,那么可以将散列码缓存在对象内部,而不是每次请求都计算散列码:

    private volatile int hasCode;
    @Override
    public int hashCode() {
        int result = hasCode;
        if (result == 0) {
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            hashCode = result;
        }
        return result;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值