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

Posted 写Bug的渣渣高

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么重写 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中重写equals方法为啥要重写hashcode方法说明

JAVA中重写equals()方法为什么要重写hashcode()方法说明

java克隆clone()方法和相等equals()方法的重写

java中重写Object类的equals方法为啥要重写hashcode方法?不重写可以吗?

总结,为什么要重写hashset的hashcode()和equals()?