为什么重写 equals 时一定要重写 hashCode?
2026年01月19日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/
面试考察点
当面试官提出这个问题时,他不仅仅是希望你背诵一条规则,更旨在考察以下几个层面:
- 对 Java 对象模型的基础理解:是否理解
equals和hashCode方法在Object类中的默认实现及其设计目的。 - 对方法契约(Contract)的掌握:是否熟悉并理解 Java SE 规范中为
hashCode方法定义的 “通用约定(General Contract)”,尤其是与equals方法相关联的关键条款。 - 对核心集合框架底层机制的洞察:是否能联系到以
HashMap、HashSet、Hashtable为代表的基于哈希表(Hash Table)的集合类的工作原理,并理解不遵守约定将如何破坏这些数据结构的基本逻辑和正确性。 - 实践意识和严谨性:这是否是你编码时的 “肌肉记忆”?能否识别出在什么场景下(例如,将自定义对象用作
HashMap的键)必须遵守此规则,以避免潜在的、难以调试的 bug。
核心答案
重写 equals 时必须重写 hashCode,是为了遵守 hashCode 方法的通用约定(General Contract)。该约定的核心条款是:如果两个对象根据 equals 方法判断是相等的,那么调用这两个对象的 hashCode 方法必须返回相同的整数值。
反之则不要求:hashCode 相等的两个对象,equals 可以不等。但违反此约定,会导致依赖哈希码的集合类(如 HashMap, HashSet)行为异常,无法正确存储和检索对象。
深度解析
原理/机制
- 默认行为:
Object类的默认equals比较对象地址,默认hashCode通常根据对象地址生成。这保证了默认情况下约定是成立的。 - 约定的意义:此约定是 Java 集合框架中哈希表(Hash Table) 数据结构正常工作的基石。哈希表通过
hashCode值快速定位对象所在的 “桶(bucket)”,再在桶内使用equals进行精确匹配。 - 违反约定的后果:假设只重写了
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。这正是因为 p1 和 p2 的 hashCode 不同,导致 HashMap 在错误的存储位置进行查找。
最佳实践
- 总是同时重写:在 IDE(如 IntelliJ IDEA, Eclipse)中生成
equals和hashCode方法时,它们总是成对出现。应使用此功能。 - 使用相同的字段:用于计算
hashCode的字段集合,必须是用于equals比较的字段集合的子集(通常就是完全相同的字段集)。 - 保证一致性:在对象的生命周期内,只要用于
equals比较的关键信息未被修改,hashCode值就应该保持不变。这也是为什么强烈不建议将可变对象用作HashMap的键。如果键对象的hashCode在存入后发生变化,你将无法再通过该键找到之前存入的值。 - 使用工具类:优先使用
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() 等工具方法来确保简洁性和正确性。