为什么重写equals方法,还必须要重写hashcode方法

Posted wdy00000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么重写equals方法,还必须要重写hashcode方法相关的知识,希望对你有一定的参考价值。

原因:
1.为了提高效率
采取重写hashcode方法,先进行hashcode比较,如果不同,那么就没必要在进行equals的比较了,这样就大大减少了equals比较的次数,这对比需要比较的数量很大的效率提高是很明显的,一个很好的例子就是在集合中的使用。

我们都知道java中的List集合是有序的,因此是可以重复的,而set集合是无序的,因此是不能重复的,那么怎么能保证不能被放入重复的元素呢,但靠equals方法一样比较的话,如果原来集合中以后又10000个元素了,那么放入10001个元素,难道要将前面的所有元素都进行比较,看看是否有重复,这个效率可想而知,因此hashcode就应遇而生了,java就采用了hash表,利用哈希算法(也叫散列算法),就是将对象数据根据该对象的特征使用特定的算法将其定义到一个地址上,那么在后面定义进来的数据只要看对应的hashcode地址上是否有值,那么就用equals比较,如果没有则直接插入,只要就大大减少了equals的使用次数,执行效率就大大提高了。

2.为了保证同一个对象
保证在equals相同的情况下hashcode值必定相同,如果重写了equals而未重写hashcode方法,可能就会出现两个没有关系的对象equals相同的(因为equal都是根据对象的特征进行重写的),但hashcode确实不相同的。

hash类存储结构(HashSet、HashMap等等)添加元素会有重复性校验,校验的方式就是先取hashCode判断是否相等(找到对应的位置,该位置可能存在多个元素),然后再取equals方法比较(极大缩小比较范围,高效判断),最终判定该存储结构中是否有重复元素。

总结:
1.提高效率。hash类型的存储结构,添加元素重复性校验的标准就是先取hashCode值,后判断equals()。重写后,使用hashcode方法提前校验,可以避免每一次比对都调用equals方法。

2.保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。

重写后两个方法的关系:

equals()相等的两个对象,hashcode()一定相等;
hashcode()不等,一定能推出equals()也不等;
hashcode()相等,equals()可能相等,也可能不等。
所以先进行hashcode()判断,不等就不用equals()方法了。
但equels是是根据对象的特征进行重写的,有时候特征相同,但hash值不同,也不是一个对象。 所以两个都重写才能保障是同一个对象。

添加链接描述

为什么重写 equals 方法 () 就要重写 hashCode ()

本文概要:
讲述为什么需要重写 equals 方法, 又为什么重写了了 equals 就要重写 hashCode 方法.
然后讲述这样做的好处

为什么要重写 equals 方法

这个 equals 方法是 Object 方法, 我们创建的每个类都是继承自 Object, 但是

    public boolean equals(Object obj) 
        return (this == obj);
    

如果不重写 equals 方法, 那么 equals 比较的就是地址.

我们再来想一想, 为什么就需要这个 equals 方法呢, 对象比较地址不是挺好的吗, 不是同一个地址, 那么就不是同一个对象啊.
但是你想想, 面对对象语言, 是不是实际上是模拟现实的, 比如说模拟显示中某一类, 某一个对象.
比如说 Class car ,Class student, 分别就代表了汽车, 学生.
又比如说, 你如何判断学生是不是同一个学生, 你思考, 比如说用名字来比较啊, 用身份证比较,所以说, 我们很多时候其实并不比较地址.
再想想, 如果就真的只比较地址?

1 == 1, 你觉得正确吗, 我觉得不正确, 这俩不是同一个 1, 他们俩是两个对象, 是"不等于的"
你就觉得这肯定很荒谬, 所以说我们来看看 Integer 如何判断两个 Integer 是否相等的.

    public boolean equals(Object obj) 
	    // 先比较类型
        if (obj instanceof Integer) 
            return value == ((Integer)obj).intValue();
        
        return false;
    

并且因为 Integer 有自动装箱拆箱功能, 即使传入普通的 int, 也会装箱成 Integer 继续比较.
然后看看逻辑, intValue ()方法

    public int intValue() 
        return value;
    

所以说,Integer 比较的是 int 类型值, 而不是对象的地址.

如果不重写 equals 方法呢?

这里要注意 ,如果该对象需要作为 Hash Map 的 Key 的话, 那么不重写 equals 方法可是会出现大问题的.

    /**
     * 这里测试一个自定义对象作为 HashMap key,但是却不重写 equals方法
     *
     * @return
     * @author ggzx
     * @create 2023/3/11 7:59
     */
    @Test
    public void testKeyWithoutEquals()
        // 我们通过 for 往 HashMap 里面传入很多对象,但是不重写 equals 方法
        HashMap<Object,String> objectMap = new HashMap<>();
        for (int i = 0; i < 100; i++) 
            Object o = new Object();
            objectMap.put(o,"ggzx");
        
        System.out.println(objectMap.size());
    

结果:100

那为什么又要重写 hashCode 方法呢

hashCode 也是 Object 的方法, 如果不重写, 默认的是把对象的底层地址通过某种方式映射到 int 范围之内, 这里要注意, 既然映射到 int 范围之内, 那么是存在一种几率发生碰撞.
重写 hashCode 方法, 可以减少在插入键值对到 hashMap 的过程中比较的次数.

首先现在有一个元素比较多的 hashmap, 然后我插入一个元素, 计算他的 hash 值, 然后通过 (n - 1) & hash 找到他要映射到的位置, 创建很多个对象, 所有对象 equals 比较内部值, 且所有对象内部值相等, 但是不重写 hashCode 方法, 查看结果

    @Test
    public void testKeyWithoutHashCodeMethod()
        HashMap<HashMapTestClass,String> map = new HashMap<>();
        for (int i = 0; i < 100; i++) 
            HashMapTestClass ggzx = new HashMapTestClass("ggzx");
            map.put(ggzx,"100");
        
        System.out.println(map.size());
    

结果:100

/**
 * <p>
 * 这里是测试HashMap用到的测试类
 * </p>
 *
 * @author ggzx
 * @since 2023/3/11
 */
public class HashMapTestClass 

    private String name;

    public HashMapTestClass() 

    

    public HashMapTestClass(String name) 
        this.name = name;
    

    @Override
    public boolean equals(Object obj) 
        if (obj instanceof HashMapTestClass)
            return this.name.equals( ((HashMapTestClass) obj).name);
        
        return false;
    

来想想原因: 我们使用自定义对象作为 key, 但是没有重写 hashCode 方法, 只重写 equals 方法, 但是 hashMap 不能插入重复的对象啊, 现在我们居然插入了很多个对象.
![[Pasted image 20230311081812.png]]
思考一下, 是什么原因

因为不重写 hashCode 方法, 在插入元素时, 使用 Object 的 hashCode 方法, 这种方法是根据对象的底层地址变换而来. 我们是通过 new 的方式来创建, 那么其实他们的 hashCode 相同的概率就很低.
而我们插入元素, 是先通过 hashCode 找到目标位置, 然后再判断 equals 方法是否相等, 此时如果 equals == true, 就会完成覆盖, 如果为 false, 那么就代表俩个对象不等, 就通过尾插法插入到链表末尾.
如果我们不重写 hashCode 方法, 如果该对象作为 hashMap 的 key, hashMap 将无法完成去重.

hashCode 方法决定了该去检索哈希表中的哪一行, 但是实际上检索的过程中可能会冲突, 比如说两个对象, 通过 hashCode 算出来的索引值都是在同一个位置, 那么此时,equals 就是起着决定性作用了.
但是如果 hashCode 方法没有设计好, 那么就可能出现一种情况, 很多个对象都可能在同一个链表上, 会拉低效率, 因为我们需要多次判断链表上/红黑树上的元素是否相等.

有两种极端, 假如 hashCode 几乎不会返回相同的值, 或者是 hashCode 只会返回相等的值. (为什么前者用的是几乎不会呢, 因为对象可能有很多, 但是 hashCode 范围只会在 int 范围内. 而相等, 就是比如我们重写一个 hashCode 方法只返回 1)
前者几乎不会返回相同的 hashCode, 此时如果要存入 HashMap ,元素基本上都是分散的
为啥说基本上的, 如果你很了解, 这里就跳过了, 因为存入数据会经历这几个步骤.
首先前提是, 比如说重写的 hashCode 方法很差, 或者没重写
hash = hashCode ^ hashCode >>> 16;// 第一次扰动, 注, 如果 key == 0, hash == 0
index = (n-1) & hash ;// 得到存储的位置,
如果很精简的描述, 每个元素 hashCode 基本上都不同, 但是如果把这些 hashCode 值映射到一个 <数组容量大小范围内,那么很有可能碰撞的啊,比如说1000个不同的hashCode,虽然很多个hashCode分散了,也是存在拥有不同的hashCode对象放在同一个位置上了> 如果还想了解更多, 可以看看我主页中前面的 (n-1) & hash 的研究, 你会觉得很有意思.
言归正传, 而如果只返回 1 ,你想想会出现啥现象, 是不是计算出来的元素应该存储的位置 index, 是相同的, 这不就是链表了吗, 那么还用哈希表干啥.

所以说, 好的 hashCode 方法, 肯定是能够减少不同元素之间的碰撞的, 减少 equals 方法比较次数.

在稍微拓展一点点, 想到 HashMap 就应该想到 HashSet,

    public boolean add(E e) 
        return map.put(e, PRESENT)==null;
    

    public V put(K key, V value) 
        return putVal(hash(key), key, value, false, true);
    

HashSet 也和 hashCode 方法脱不了干系, 不好的 hashCode 同样会影响 HashSet, 如果没有重写 hashCode 方法, 然后作为 HashSet 的 Key, 那么也失去了去重的效果.

啥, 你问我要代码, 看看上面的测试, 有一个同样也适用于 HashSet

解决内存泄漏问题

内存泄漏指的其实就是上面我谈及到的, 如果我们再极端一点, 一直存入用户传来的对象, 假如这些对象都相等, 那是不是就出现了内存泄漏了?
解决办法, 就是, 重写 equals 方法和 hashCode 方法.

以上是关于为什么重写equals方法,还必须要重写hashcode方法的主要内容,如果未能解决你的问题,请参考以下文章

重写了equals方法为何需要重写 hashCode

java 集合中重写hashCode方法和重写equals方法啥关系?

为什么重写 equals 方法 () 就要重写 hashCode ()

重写equals 方法的注意事项

为啥重写equals方法,一定要重写HashCode方法?

为什么重写equals()方法就必须重写hashCode()方法