为什么重写 equals 时一定要重写 hashCode?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

当面试官提出这个问题时,他不仅仅是希望你背诵一条规则,更旨在考察以下几个层面:

  1. 对 Java 对象模型的基础理解:是否理解 equalshashCode 方法在 Object 类中的默认实现及其设计目的。
  2. 对方法契约(Contract)的掌握:是否熟悉并理解 Java SE 规范中为 hashCode 方法定义的 “通用约定(General Contract)”,尤其是与 equals 方法相关联的关键条款。
  3. 对核心集合框架底层机制的洞察:是否能联系到以 HashMapHashSetHashtable 为代表的基于哈希表(Hash Table)的集合类的工作原理,并理解不遵守约定将如何破坏这些数据结构的基本逻辑和正确性。
  4. 实践意识和严谨性:这是否是你编码时的 “肌肉记忆”?能否识别出在什么场景下(例如,将自定义对象用作 HashMap 的键)必须遵守此规则,以避免潜在的、难以调试的 bug。

核心答案

重写 equals 时必须重写 hashCode,是为了遵守 hashCode 方法的通用约定(General Contract)。该约定的核心条款是:如果两个对象根据 equals 方法判断是相等的,那么调用这两个对象的 hashCode 方法必须返回相同的整数值。

反之则不要求:hashCode 相等的两个对象,equals 可以不等。但违反此约定,会导致依赖哈希码的集合类(如 HashMap, HashSet)行为异常,无法正确存储和检索对象。

深度解析

原理/机制

  1. 默认行为Object 类的默认 equals 比较对象地址,默认 hashCode 通常根据对象地址生成。这保证了默认情况下约定是成立的。
  2. 约定的意义:此约定是 Java 集合框架中哈希表(Hash Table) 数据结构正常工作的基石。哈希表通过 hashCode 值快速定位对象所在的 “桶(bucket)”,再在桶内使用 equals 进行精确匹配。
  3. 违反约定的后果:假设只重写了 equals 认为两个对象逻辑相等,但没重写 hashCode,导致它们的 hashCode 不同(仍基于地址)。当把它们放入 HashMap 时:
    • 它们很可能被放入不同的桶。
    • map.put(objA, valueA) 后,用 equals 相等的 objB 执行 map.get(objB),由于 hashCode 不同,程序会直接去错误的桶里查找,结果返回 null,造成“对象丢失”的假象。

代码示例

我们创建一个简单的 Person 类,并错误地只重写 equals

import java.util.HashMap;
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 正确重写了 equals, 根据 name 和 age 判断逻辑相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    // 错误:没有重写 hashCode!
    // @Override public int hashCode() { ... }

    public static void main(String[] args) {
        Person p1 = new Person("张三", 25);
        Person p2 = new Person("张三", 25); // 逻辑上与 p1 相等

        HashMap<Person, String> map = new HashMap<>();
        map.put(p1, "Employee");

        // 关键测试:我们试图用“相等”的 p2 来获取值
        System.out.println(map.get(p2)); // 输出:null
        System.out.println(p1.equals(p2)); // 输出:true
        System.out.println(p1.hashCode() == p2.hashCode()); // 输出:false (因为未重写,使用默认)
    }
}

运行上述代码,虽然 p1.equals(p2)true,但 map.get(p2) 返回 null。这正是因为 p1p2hashCode 不同,导致 HashMap 在错误的存储位置进行查找。

最佳实践

  1. 总是同时重写:在 IDE(如 IntelliJ IDEA, Eclipse)中生成 equalshashCode 方法时,它们总是成对出现。应使用此功能。
  2. 使用相同的字段:用于计算 hashCode 的字段集合,必须是用于 equals 比较的字段集合的子集(通常就是完全相同的字段集)。
  3. 保证一致性:在对象的生命周期内,只要用于 equals 比较的关键信息未被修改,hashCode 值就应该保持不变。这也是为什么强烈不建议将可变对象用作 HashMap 的键。如果键对象的 hashCode 在存入后发生变化,你将无法再通过该键找到之前存入的值。
  4. 使用工具类:优先使用 java.util.Objects 类的 hash(Object... values) 方法来生成 hashCode,它简洁且能有效处理 null 值。
    @Override
    public int hashCode() {
        // 使用 Objects.hash 计算 name 和 age 的哈希码组合
        return Objects.hash(name, age);
    }
    

常见误区

  • hashCode 需要返回唯一值”:错。hashCode 是哈希值,冲突(不同对象拥有相同哈希码)是允许且常见的。哈希表的实现(如 HashMap)已经通过链表或红黑树解决了冲突问题。
  • hashCode 可以用随机数实现”:大错特错。这会导致同一对象每次调用 hashCode 返回不同值,完全破坏了哈希集合的稳定性。
  • “系统性能要求不高,可以不重写”:这是对代码正确性的忽视。只要对象有可能被放入哈希集合,就必须遵守约定,否则就是埋下 bug。

总结

重写 equals 必须重写 hashCode,这是一条必须遵守的 Java 编程铁律,其根本目的是维系对象在哈希集合中的正确行为。在实现时,应使用 Objects.hash() 等工具方法来确保简洁性和正确性。