equals和hashcode终极解答

为什么重写equals就必须重写hashcode?

我们必须先了解hash桶。水桶用来存放水,而hash桶用来存放多个hash值,hash算法负责将hash值分配到hash桶里,而相同的hash值始终位于相同的桶内。当存储元素时首先计算出hash值,然后找到对应的hash桶,把值放在该桶里。查找元素时,同样计算出hash值并找到对应的hash桶,随后将桶内所有元素取出一一对比equals,即完成查找。
为什么要利用hash桶呢?这样是为了减少需要用equals比较的元素数量。想象下传统的方案中我们将所有元素一一对比equals,而现在只需和一个hash桶内元素对比即可。
那如果不重写hashcode,即可能出现equals相同的元素其hashcode不同。假如我们已经在set中插入了一个元素,现在插入一个跟它equals的元素,但由于两者hashcode不同,因此被分配到不同的桶中,导致这个元素被添加进set,这违反了set的规则!
从上述可以看出,hashcode的作用是辅助equals的判断,它主要考虑的是效率。

重写equals

重写equals具有许多陷阱,稍不小心就会违反下面这些原则:
- 自反性
- 相对性
- 传递性
- 永久性
- 任何非NULL对象不等于NULL
接下来我们先给出一个标准实现,然后一一解释

    @Override
    public boolean equals(Object o) {
        //自检查
        if(this == o )
            return true;

        //空检查
        if (o == null)
            return false;

        //类型检查和转换
        if (getClass() != o.getClass())
            return false;

        Car car = (Car)o;

        // 成员比较
        return Objects.equals(name, car.name);
    }
  • 函数参数
    函数参数一定要为object,考虑如下情况:
     Car a = new Car("my");
     Object b = new Car ("my");

     boolean answer = a.equals(b);

这里answer将为false。因此java的方法重载基于静态类型,因此这里调用的是Obeject.equals方法。当我们使用集合时会大量遇到这种情况。

  • 自校验
    equals是一个被经常调用的基本方法,因此需要注意它的性能,自校验可以极大的提高性能。

  • 空校验
    空校验保证上述最后一条原则,也避免了NullPointerException

  • 类型检查和转换
    这里有两种实现方式,我们采取的方式将导致所以子类equals返回false,这可能不是我们想要的结果,因为有些子类仅添加功能而不增加成员变量,那么它们应该允许和父类相等。对hibernate或者spring这些会在运行中产生子类的框架来讲这点尤为重要。因此,我们衍生出另一种做法采用instanceof

    if (!(o instanceof Car))
    return false;

    但需要注意的是这种做法会引起其他问题。例如SUV派生自Car同时添加了新的成员变量并覆盖equals方法,那么car.equals(suv)将返回true,而suv.equals(car)将返回false,这显然违反了相对性。
    当然我们可以在suv的equals中检查object是否为car来避免这个问题,但这种做法违反了传递性,比如:

     Car a = new Car("my");
     SUV s1 = new SUV("my", "1");
     SUV s2 = new SUV("my", "2");

    a分别和s1以及s2满足equals,但s1和s2不满足。
    所以这里有两种做法:使用getclass同时记住所有的子类和父类不能equals;使用instanceof方法同时设置equals为final

  • 成员比较
    基本类型使用==,引用类型使用equals,对于可能为空的字段需要检验是否为NULL。因此建议采用Objects.equals方法

    重写hashcode

    重写hashcode需要注意几点:

  • 选择字段
    不能选择equals函数没有采用的字段,这样可能导致两个equals的对象其hashcode却不相同!换句话即采用equals字段的子集。

  • 一致性
    如果不小心将已经添加到hashset的对象作出了修改,由于hash值不会重新计算,hashset的内部结构也不会改变,因此接下来即使使用相equals的对象也无法查找到该元素!即使使用该元素本身也不行!所以我么需要尽量使用不可变变量来计算hash值。

    计算过程

    把某个非0的常数值,比如17,保存在一个名为result的int类型的变量中。
    对于对象中的每个域,做如下操作:
    为该域计算int类型的哈希值c:
    如果该域是boolean类型,则计算(f?1:0)
    如果该域是byte、char、short或者int类型,则计算(int)f
    如果该域是long类型,则计算(int)(f^(f>>>32))
    如果该域是float类型,则计算Float.floatToIntBits(f)
    如果该域是double类型,则计算Double.doubleToLongBits(f),然后重复第三个步骤。
    如果该域是一个对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
    如果该域是数组,则要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。
    把上面计算得到的hash值c合并到result中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值