为什么重写equals一定要重写hashcode

在原始的Object类中

默认equals比较规则是通过“==”来进行判断,这样比较的是两个对象的内存地址,默认的hashcode方法返回的是对象的内存地址由哈希算法转换成的一个整数,实际上指的的也是内存。哈希算法具有一定的偶然性,不同的内存地址可能计算出相同的哈希值。

对于原始的hashCode()和equals() 方法来说,通过equals() 比较两个对象相等,说明这两个对象的内存地址相同,进而知hashcode也是相同的。

在实际使用中,大多数场景下,如HashMap中存放自定义对象类作为key

当用 HashMap 存入自定义的类时,如果不重写这个自定义类的 equals 和 hashCode 方法,得到的结果会和预期的不一样。

举例,定义一个 HashMapKey.java 的类,只有一个属性 id :

public class HashMapKey {
    private Integer id;
    
    public HashMapKey(Integer id) {
        this.id = id;
    }
    public Integer getId() {
        return id;
    }
}

测试类如下:

public class TestHashMap {
    public static void main(String[] args) {
        HashMapKey k1 = new HashMapKey(1);
        HashMapKey k2 = new HashMapKey(1);
        HashMap<HashMapKey, String> map = new HashMap<>();
        map.put(k1, "程序猿杂货铺");
        System.out.println("map.get(k2) : " + map.get(k2));
    }
}

定义了两个 HashMapKey 对象,id 都是 1,创建一个 HashMap 对象,通过 put 方法把 k1 和一串字符放入到 map里,最后用 k2 去从 HashMaP里得到值,因为 k1 和 k2 值是一样的,理论上我们是可以用这个键获取到对应的值的,看似符合逻辑,实则不然,它的执行结是:map.get(k2) : null。出现这个情况的原因有两个:

  • 没有重写 hashCode 方法
  • 没有重写 equals 方法。

存的是k1,但是要通过k2去取,问题本质是判断两个对象是否相同

当往 HashMap 里放 k1 时,首先会调用 HashMapKey 这个类的 hashCode 方法计算它的 hash 值,随后把 k1 放入 hash 值所指引的内存位置

但是在 HashMapKey 中没有重写 hashCode 方法,所以这里调用的是顶级父类Object 类的 hashCode 方法,而 Object 类的 hashCode 方法返回的 hash 值其实是 k1 对象的内存地址(假设是 0x100)。

如果是调用 map.get(k1)查询map,那么接下来还是会再次调用Object的hashCode 方法(还是返回 k1 的地址 0x100),随后根据得到的 hash 值,能很快地找到 k1。

但通过map.get(k2)查询map时,还是会调用Object类的 hashCode方法计算 k2 的 hash值,得到的是 k2 的内存地址(假设是 0x200)。由于 k1 和 k2 是new出来的两个不同的对象,具有不同的内存地址空间,也就是说它们的 hash 值一定不同。所以通过k2是无法得到k1

到达此处说明
hashcode不同,两个对象一定不同

当重写 hashCode 方法后

@Override
public int hashCode() {
   return id.hashCode();
}

此时因为 hashCode 方法返回的是 id 的 hash值,所以此处 k1 和 k2 这两个对象的 hash 值就变得相等了。

存 k1 时,是根据它 id 的 hash 值,假设这里是 103,把 k1 对象放入到对应的位置。而通过 k2 取时,是先计算它的 hash 值,由于 k2 的 id 也是 1,这个值也是 103,随后到这个位置去找。按道理应该可以找到。但运行结果还是会出乎意料:map.get(k2) : null

HashMap 是用链地址法来处理冲突,也就是说,在 103号位置上,有可能存在着多个用链表形式存储的对象。它们通过 hashCode 方法返回的 hash 值都是 103。
为什么重写equals一定要重写hashcode_第1张图片

当通过 k2 的 hashCode 到 103号位置查找时,确实会得到 k1。但 k1 有可能仅仅是和 k2 具有相同的 hash值,但未必和 k2 相等。

到达此处说明
hashcode相同,两个对象也不一定不同
两个对象不同,hashcode不一定不等

判断完hashcode相同后,这个时候就需要调用 HashMapKey 对象的 equals 方法来判断两者是否相等了。

由于在 HashMapKey 对象里没有定义 equals 方法,系统就不得不调用 Object 类的 equals 方法,由于 Object 的原生equals方法是根据两个对象的内存地址来判断,而k1和k2是new出来的两个对象具有不同的内存空间,所以 k1 和 k2 一定不会相等,

这就是为什么通过 map.get(k2) 依然得到 null 的原因。

当继续重写 equals 方法后

在这个方法里,只要两个对象都是 Key 类型,而且它们的 id 相等,它们就相等。

@Override
public boolean equals(Object o) {
     if (o == null || !(o instanceof HashMapKey)) {
         return false;
     } else {
         return this.getId().equals(((HashMapKey) o).getId());
     }
}

至此over

判断两个对象是否相同
先求出hashcode(),比较其值是否相等;
若相等;
再比较equals();
若相等;
则认为他们是相等的。若equals()不相等则认为他们不相等。

两个对象相等,hashcode一定相等

⚠️ 如果你需要要在 HashMap 的“键”部分存放自定义的对象,一定要重写 equals 和 hashCode 方法。

参考
https://zhuanlan.zhihu.com/p/61307537

如果只重写了equals不重写hashcode呢

在Object类中,equals方法默认使用“==”号来对两个对象进行判断,比较的是地址

这种判断方式本质上没错,但是不太符合实际需求,就好比在两个不同的超市里面都有矿泉水,但是因为地址值不同,在使用equals做判断时,这两个超市的矿泉水就会返回为false;因此在实际开发中我们往往需要重写Object的equals方法。

在Object类中,hashCode方法根据当前对象地址返回一个整型的hash值。哈希算法具有偶然性,相同对象地址返回hash值一定是一样,不同对象地址返回hash值也可能一样。

在java底层集合框架中,为了提高查询效率,往往使用hashCode方法来确定元素的保存位置。

定义一个Sudent类只重写了Object的equals方法,没有重写hashCode方法

为什么重写equals一定要重写hashcode_第2张图片
测试
为什么重写equals一定要重写hashcode_第3张图片
输出
为什么重写equals一定要重写hashcode_第4张图片
按照我们的常规理解,只要两个学生对象的id和name是一样的,我们就可以认为这两个学生对象指的是同一个人,因此重写了equals方法,让Student对象只要name和id相同就返回true,而且并没有重写hashCode方法。

在测试中,新建了两个Student对象,并且让他们的id和name完全一样,在调用equals方法时,返回为true,说明这两个对象时相等的。但是由于没有重写hashCode方法,所以这两个对象调用的hashCode还是Object类那里的hashCode方法,并且他们的值并不相等。

由此可以得出结论:重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回不同的结果。

那么这样会有什么影响呢?

在java底层的集合框架中(如HashMap,HashSet等),为了提高查询的效率,在确定某个对象的存储位置时,往往需要通过调用对象的hashCode方法来实现。

例如在上例中,我们把新建出来的两个Student对象放入HashSet集合中:
为什么重写equals一定要重写hashcode_第5张图片
按照我们主观理解,这两个对象equals为true,那么HashSet应该主动帮我们去重,最终的HashSet中应该只保留1个对象,即最终输出的HashSet的size为1,但是我们得到的结果却是2:

是因为HashSet的底层其实就是HashMap,当存放对象时,先调用这个对象的hashCode方法计算存放位置。由于student没有重写hashCode方法,所以使用的是Object类的hashcode方法,所以存放的位置在底层数组上是不一样的,不会触发HashSet的去重功能,而对于程序员来说,两个相同的对象却会在HashSet中出现多次。

参考
https://zhuanlan.zhihu.com/p/102248677

重写了equals和hashcode后
为什么重写equals一定要重写hashcode_第6张图片
为什么重写equals一定要重写hashcode_第7张图片
为什么重写equals一定要重写hashcode_第8张图片

总结

根据面向对象思想,只要值相同,就为相同两个对象

所以重写equals比较值。出现“equals相等,但是hashcode不等”

在实际使用时,比如在集合HashMap HashSet中,先去计算hashcode,若相等再去比较equals

若hashcode不同,则认为是不同的对象,无需再去比较equals

所以重写了equals,必须要重写hashcode


个人博客:wgzz.top
个人公众号:程序员WeiG
欢迎关注

你可能感兴趣的:(后端开发笔记,java,后端,equals,hashcode,java基础)