为什么重写 equals 方法时必须同时重写 hashCode 方法?

简介: 本文探讨了 Java 中 `hashCode` 方法的作用及其与 `equals` 方法的关系,解释了为什么重写 `equals` 方法时必须同时重写 `hashCode` 方法,并提供了如何正确重写 `hashCode` 方法的示例。

本文由 Java 中常见的面试题「为什么重写 equals 方法时必须同时重写 hashCode 方法?」所引出。渐进式探讨关于 hashCode 的三个问题:hashCode 方法的作用以及 hashCode 方法与 equals 方法的关系?为什么重写 equals 方法时必须同时重写 hashCode 方法?以及如何重写 hashCode 方法?

我们知道,Java 中 Object 类是所有类的父类,而 hashCode 是 Object 类中定义的方法,所以每个类都会默认拥有一个 hashCode 方法。

如下为 hashCode 方法在 Object 类中的定义:

java

代码解读

复制代码

package java.lang;

public class Object {

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * `java.util.HashMap`.
     *
     * The general contract of `hashCode` is:
     *
     * a) Whenever it is invoked on the same object more than once during
     *    an execution of a Java application, the `hashCode` method must
     *    consistently return the same integer, provided no information
     *    used in `equals` comparisons on the object is modified.
     *    This integer need not remain consistent from one execution of an
     *    application to another execution of the same application.
     *
     * b) If two objects are equal according to the `equals(Object)` method,
     *    then calling the `hashCode` method on each of the two objects must
     *    produce the same integer result.
     *
     * c) It is not required that if two objects are unequal according to the
     *    `equals(Object)` method, then calling the `hashCode` method on each of
     *    the two objects must produce distinct integer results.
     *    However, the programmer should be aware that producing distinct integer
     *    results for unequal objects may improve the performance of hash tables.
     */
    @IntrinsicCandidate
    public native int hashCode();

    /**
     * Indicates whether some other object is "equal to" this one.
     *
     * @apiNote
     * It is generally necessary to override the `hashCode` method whenever this
     * method is overridden, so as to maintain the general contract for the `hashCode`
     * method,  which states that equal objects must have equal hash codes.
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }
}

可以看到,hashCode 方法用于生成一个整数,其是一个原生方法且使用 @IntrinsicCandidate 注解修饰,表示其实现完全依赖于 JVM,不同的 JVM 可能有不同的实现,常见的实现有使用伪随机数等。

为什么 Object 类中要定义一个 hashCode 方法呢?此外我们还注意到,equals 方法同样被定义在 Object 类中,这两个方法之间有什么关系呢?

1 hashCode 方法的作用以及 hashCode 方法与 equals 方法的关系

Java 中,hashCode 方法主要是为了配合哈希表来使用的。

哈希表是存储键值(Key Value)对数据的一种数据结构。其通过将键映射到表中一个位置来访问数据,以加快查找速度,这个映射函数即被称为哈希函数(Hash Function)。Java 中的 HashSet、Hashtable 与 HashMap 均使用了哈希表。

假定我们想实现一个 Set,其存放的数据是不允许重复的,如果不借助哈希表,应该怎么来实现呢?

我们能想到的一个办法是:当一个对象要被存入 Set 时,调用其 equals 方法与 Set 中的已有对象逐个进行比较,只有其与所有已有对象都不 Equal 时才可将其存入 Set。而一般来说,equals 方法的实现都比较重,需要将对象中的各个关键字段逐个进行比较,这在存放的对象特别多的时候效率会非常低下。

而如果借助哈希表来实现呢?我们知道 Java 中 HashSet 是借用 HashMap 来实现的,HashMap 是怎么在添加记录的时候提升效率的呢?

HashMap 存储结构为哈希表,在添加一个键值对时,有如下步骤:

  • a) 调用键对象的 hashCode 方法获取其哈希值;
  • b) 与现有哈希值逐个进行比较,若不相等,则直接存入哈希表;
  • c) 若有相等的,再调用键对象的 equals 方法进行比较,若不 Equal,则存入哈希表(此两个对象哈希冲突,需要增加一个链表来存放对象的引用);若 Equal,则不存入。

可以看到,借助哈希表实现去重集合的话,因首先会判断哈希值是否相等,只有不相等时才会调用 equals 方法,所以只要哈希算法足够好,就会省去很多 equals 方法的调用。

此外,哈希算法选用得当的话(理想的哈希算法是针对不同的对象,生成的哈希值可以均匀分布在整个 int 区间上,现实中是越接近越好),哈希表的检索效率会非常高,没有一次哈希冲突的话,检索记录的时间复杂度为 O(1);最坏情况,hashCode 全部相等,存储结构完全变成了一个链表,那么检索记录的时间复杂度会变为 O(N)。

总结该部分,我们可以看到:hashCode 一般与 equals 一起使用,两个对象作「相等」比较时,因判断 hashCode 是判断 equals 的先决条件,所以两者使用必须遵循一定的约束。hashCode 方法的注释上即说明了其与 equals 方法一起使用时需要遵循的三个通用约定:

  • 同一对象多次调用 hashCode 方法,必须返回相同的整数值;
  • 对于两个对象 a 和 b,若 a.equals(b),则 a.hashCode() 与 b.hashCode() 必须相同;
  • 对于两个对象 a 和 b,若 !a.equals(b),则 a.hashCode() 并不一定得与 b.hashCode() 不相同。

知道了 hashCode 方法的作用以及 hashCode 方法与 equals 方法的关系后,下面探讨一下为什么重写 equals 方法时必须同时重写 hashCode 方法。

2 为什么重写 equals 方法时必须同时重写 hashCode 方法?

上面介绍了 hashCode 方法注释上列出的三个通用约定,equals 方法的注释上也有这么一句话:「每当重写 equals 方法时,都需要重写 hashCode 方法,这样才没有破坏 hashCode 方法的通用约定,即:两个对象为 Equal 的话(调用 equals 方法为 true), 那么这两个对象分别调用 hashCode 方法也需要返回相同的哈希值」。

所以只重写 equals 方法不重写 hashCode 方法的话,可能会造成两个对象调用 equals 方法为 true,而 hashCode 值不同的情形,这样即可能造成异常的行为。

最后探讨一下如何重写 hashCode 方法。

3 如何重写 hashCode 方法?

下面定义了一个 User 类:

java

代码解读

复制代码

// src/test/java/com/example/demo/model/User.java
package com.example.demo.model;

public class User {
    private final String name;
    private final Integer age;
    private final Gender gender;

    public User(String name, Integer age, Gender gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public enum Gender {
        MALE,
        FEMALE
    }
}

该类有三个属性:name、age 和 gender。

若不重写 User 类的 hashCode 与 equals 方法的话,则会使用 Object 类定义的默认实现,即:hashCode 是 JVM 生成的一个伪随机数,equals 比较的是两个引用的地址。

下面测试代码新建了两个逻辑上「相等」的 User 对象:user1 与 user2,然后比较 user1.equals(user2) 与 user1.hashCode() == user2.hashCode(),发现结果均为 false;然后将此两个对象作为键放入 HashMap 后,查看 HashMap 的 size,结果 为 2,表示两个对象均被添加了进去。这即是不重写 hashCode 与 equals 方法发生的「异常行为」。

java

代码解读

复制代码

// 不重写 User 类的 hashCode 与 equals 方法
User user1 = new User("Larry", 18, User.Gender.MALE);
User user2 = new User("Larry", 18, User.Gender.MALE);

System.out.println(user1.equals(user2)); // false
System.out.println(user1.hashCode() == user2.hashCode()); // false

Map<User, Boolean> map = new HashMap<>();
map.put(user1, true);
map.put(user2, true);
System.out.println(map.size()); // 2

下面尝试在 User 类中重写一下 hashCode 与 equals 方法:

java

代码解读

复制代码

// src/test/java/com/example/demo/model/User.java
package com.example.demo.model;

import java.util.Objects;

public class User {
    // ...

    @Override
    public int hashCode() {
        int result = name.hashCode();
        result = 31 * result + Integer.hashCode(age);
        result = 31 * result + gender.toString().hashCode();
        return result;

        // Another way: using Object.hash()
        // return Objects.hash(name, age, gender);
    }

    @Override
    public boolean equals(Object obj) {
        if (null == obj) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof User user)) {
            return false;
        }
        return Objects.equals(user.name, name)
                && Objects.equals(user.age, age)
                && Objects.equals(user.gender, gender);
    }
}

重写 hashCode 方法使用的算法如下:

hash=val[0]×31(n−1)+val[1]×31(n−2)+...+val[n−1]\boldsymbol {hash} = {val[0] \times 31^{(n-1)} + val[1] \times 31^{(n-2)} + ... + val[n-1]}hash=val[0]×31(n−1)+val[1]×31(n−2)+...+val[n−1]

该算法公式借用了 JDK 中「String.hashCode()」的实现逻辑,即:按属性依次计算哈希结果,当前属性的哈希结果为上一个属性的哈希结果乘以 31 并加上当前属性的哈希值(currentVal = 31 * previousVal + hash(currentPropertity)),直至所有属性计算完毕,最终的结果即为对象的哈希值。至于为什么要乘以 31 呢?原因是在于:其是一个奇素数,可以更好的保留信息,若是偶数的话,乘一个偶数相当于移位,超出的话会丢失信息;此外乘以 31 会被现代虚拟机优化为移位和减法来实现(31 * i == (i << 5) - i),非常的高效。此外还可以直接调用 Objects.hash(prop1, prop2, prop3, ...) 来获取一个哈希值。

重写 equals 方法的逻辑非常简单,即:判断是否为 User 对象且所有字段是否一致。

再次使用如下代码测试一下,发现 user1.equals(user2) 与 user1.hashCode() == user2.hashCode() 结果均为 true;调用 HashMap 的 put 方法将 user1 与 user2 均 put 后,size 为 1。这样即符合了我们的期望。

java

代码解读

复制代码

// 重写 User 类的 hashCode 与 equals 方法
User user1 = new User("Larry", 18, User.Gender.MALE);
User user2 = new User("Larry", 18, User.Gender.MALE);

System.out.println(user1.equals(user2)); // true
System.out.println(user1.hashCode() == user2.hashCode()); // true

Map<User, Boolean> map = new HashMap<>();
map.put(user1, true);
map.put(user2, true);
System.out.println(map.size()); // 1


转载来源:https://juejin.cn/post/7369044948330479668

相关文章
|
6月前
|
安全 Java 编译器
【Java基础】面向对象三大特性、重载 vs 重写、接口 vs 抽象类(附《面试背诵版》+《自测题》)
本文系统梳理Java面向对象核心知识:详解封装、继承、多态三大特性及实现要点;清晰对比重载(编译时多态)与重写(运行时多态)的语法与语义差异;深入剖析抽象类(is-a、代码复用)与接口(can-do、多实现)的本质区别;涵盖面试高频考点、典型易错点及速记口诀,助你高效掌握并应对技术面试。
|
6月前
|
存储 安全 Java
【Java基础】String不可变性、String vs StringBuffer vs StringBuilder、常量池、intern()方法(附《思维导图》+《面试考点背诵版》)
本文系统解析Java字符串核心机制:String不可变性(`final char[]`实现)、String/StringBuffer/StringBuilder三者对比(可变性/线程安全/性能)、字符串常量池(JDK7+移至堆)及`intern()`方法(JDK7+直接存堆引用)。
|
存储 缓存 安全
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
本文详细解析ConcurrentHashMap的实现原理,大厂高频面试,必知必备。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
|
敏捷开发 算法 测试技术
软考中级软件设计师专项-软件工程专题上篇
本篇章精讲软考中级软件设计师“软件工程”核心内容,涵盖CMM/CMMI成熟度模型、瀑布/螺旋/敏捷等开发模型、系统测试与维护策略及McCabe复杂度等考点,结合例题解析,助力构建全生命周期知识体系,精准突破考试重难点。
778 1
软考中级软件设计师专项-软件工程专题上篇
|
机器学习/深度学习 人工智能 移动开发
一文搞懂 FFN / RNN / CNN 的参数量计算公式 !!
一文搞懂 FFN / RNN / CNN 的参数量计算公式 !!
1669 3
|
安全 数据可视化 数据安全/隐私保护
网站建设,如何从新手到专业的详细流程及步骤
文章总结:数字化时代,网站建设是提升品牌和业务的重要手段,涵盖规划、域名注册、服务器租用、内容创建及推广等步骤,帮助新手成长为专业站长。
620 1
|
前端开发 JavaScript
什么是深拷贝;深拷贝和浅拷贝有什么区别;深拷贝和浅拷贝有哪些方法(详解)
浅拷贝适用于只复制对象的第一层属性,且这些属性不是引用类型。深拷贝适用于需要完全独立的副本,包括对象和数组的嵌套结构。选择哪种拷贝方式取决于你的具体需求和场景。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
2031 1
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
移动开发 JavaScript 前端开发
【前端用法】html5实现地理位置定位(JS获取当前地理位置的方法)
【前端用法】html5实现地理位置定位(JS获取当前地理位置的方法)
1025 0
|
Web App开发 移动开发 前端开发
uniapp环境H5运行及发行(入门必学)
uniapp环境H5运行及发行(入门必学)
3070 5

热门文章

最新文章