本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
系列文章目录
JAVA数据结构 DAY9 equals、Comparable、Comparator 与 PriorityQueue 深度解析
拓展目录
手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解
Java 中的 hashCode () 与 equals () 核心原理、契约规范、重写实践与面试全解
目录
目录
[一、溯源:Object 类中的 equals () 与 hashCode ()](#一、溯源:Object 类中的 equals () 与 hashCode ())
[1.1 equals () 方法的官方定义与默认实现](#1.1 equals () 方法的官方定义与默认实现)
[1.2 hashCode () 方法的官方定义与默认实现](#1.2 hashCode () 方法的官方定义与默认实现)
[二、核心对比:equals () 与 hashCode () 的本质区别](#二、核心对比:equals () 与 hashCode () 的本质区别)
[三、黄金契约:equals () 与 hashCode () 的三大绑定规则](#三、黄金契约:equals () 与 hashCode () 的三大绑定规则)
[契约 1:equals () 相等 → hashCode () 必须相等](#契约 1:equals () 相等 → hashCode () 必须相等)
[契约 2:hashCode () 相等 → equals () 不一定相等](#契约 2:hashCode () 相等 → equals () 不一定相等)
[契约 3:对象不变 → hashCode () 永远不变](#契约 3:对象不变 → hashCode () 永远不变)
[四、致命问题:只重写 equals () 不重写 hashCode () 的灾难](#四、致命问题:只重写 equals () 不重写 hashCode () 的灾难)
[4.1 测试场景](#4.1 测试场景)
[4.2 运行结果与问题分析](#4.2 运行结果与问题分析)
[4.3 根本原因](#4.3 根本原因)
[4.4 解决方案:同时重写两个方法](#4.4 解决方案:同时重写两个方法)
[五、源码深度解析:HashMap 如何依赖 equals () 和 hashCode ()?](#五、源码深度解析:HashMap 如何依赖 equals () 和 hashCode ()?)
[5.1 HashMap 存储 / 查找核心流程](#5.1 HashMap 存储 / 查找核心流程)
[5.2 JDK 1.8 HashMap 核心源码(简化版)](#5.2 JDK 1.8 HashMap 核心源码(简化版))
[1. 计算哈希值](#1. 计算哈希值)
[2. 查找元素(get 方法)](#2. 查找元素(get 方法))
[5.3 源码核心结论](#5.3 源码核心结论)
[六、最佳实践:如何正确重写 equals () 和 hashCode ()?](#六、最佳实践:如何正确重写 equals () 和 hashCode ()?)
[6.1 重写核心原则](#6.1 重写核心原则)
[6.2 IDEA 一键生成教程](#6.2 IDEA 一键生成教程)
[6.3 标准重写模板(通用版)](#6.3 标准重写模板(通用版))
[6.4 关键工具类:Objects](#6.4 关键工具类:Objects)
[七、进阶知识点:equals () 与 == 的区别](#七、进阶知识点:equals () 与 == 的区别)
[7.1 核心区别](#7.1 核心区别)
[7.2 实战代码演示](#7.2 实战代码演示)
[面试题 1:equals () 和 hashCode () 的作用是什么?](#面试题 1:equals () 和 hashCode () 的作用是什么?)
[面试题 2:为什么重写 equals () 必须重写 hashCode ()?](#面试题 2:为什么重写 equals () 必须重写 hashCode ()?)
[面试题 3:两个对象 equals 相等,hashCode 一定相等吗?](#面试题 3:两个对象 equals 相等,hashCode 一定相等吗?)
[面试题 4:两个对象 hashCode 相等,equals 一定相等吗?](#面试题 4:两个对象 hashCode 相等,equals 一定相等吗?)
[面试题 5:hashCode () 是唯一的吗?](#面试题 5:hashCode () 是唯一的吗?)
[面试题 6:String 类重写 equals () 和 hashCode () 了吗?](#面试题 6:String 类重写 equals () 和 hashCode () 了吗?)
[误区 1:使用可变属性参与 hashCode () 计算](#误区 1:使用可变属性参与 hashCode () 计算)
[误区 2:重写 equals 时不遵守五大规范](#误区 2:重写 equals 时不遵守五大规范)
[误区 3:认为 hashCode () 是唯一标识](#误区 3:认为 hashCode () 是唯一标识)
[误区 4:null 对象调用 equals ()](#误区 4:null 对象调用 equals ())
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在 Java 编程语言的体系中,java.lang.Object 是所有类的根父类 ,这意味着 Java 中的每一个对象,都天生拥有 Object 类中定义的核心方法。其中,equals() 和 hashCode() 是两个贯穿 Java 开发全流程、支撑哈希集合核心逻辑、面试高频考察的关键方法。
对于初级开发者而言,常常会陷入三个误区:
- 混淆
equals()和==的使用场景; - 不理解
hashCode()的真正作用,认为它是 "无用的方法"; - 只重写
equals()不重写hashCode(),导致 HashMap、HashSet 等集合出现数据重复、查找失败、内存泄漏等隐蔽 Bug。
对于中高级开发者而言,深入理解这两个方法的底层契约、源码实现、哈希表工作原理,是写出健壮、高效、无 Bug 代码的基础,也是应对大厂面试、架构设计的必备能力。
本文将从基础定义、默认实现、核心区别、官方契约、源码解析、实战案例、常见问题、面试真题 八个维度,用万字长文 全面、深度、通俗地讲解 hashCode() 和 equals(),让你彻底吃透这两个 Java 核心知识点。
一、溯源:Object 类中的 equals () 与 hashCode ()
所有 Java 类都隐式继承自 Object,因此我们首先看这两个方法在根类中的原始定义、实现逻辑、核心作用。
1.1 equals () 方法的官方定义与默认实现
官方源码
java
public boolean equals(Object obj) {
return (this == obj);
}
核心解读
- 方法功能 :判断当前对象(
this)与传入对象(obj)是否相等; - 默认实现 :直接使用
==比较两个对象的内存地址; - 返回值 :
true:两个对象是同一个对象(内存地址完全相同);false:两个对象是不同对象(内存地址不同)。
官方规范(JavaDoc)
equals() 方法在 Java 中必须遵守五大通用规范,这是所有重写该方法的类都必须遵循的准则:
- 自反性 :对于任何非空引用值
x,x.equals(x)必须返回true; - 对称性 :对于任何非空引用值
x和y,x.equals(y)为true时,y.equals(x)也必须为true; - 传递性 :对于任何非空引用值
x、y、z,x.equals(y)和y.equals(z)为true时,x.equals(z)也必须为true; - 一致性 :对于任何非空引用值
x和y,只要对象属性未修改,多次调用x.equals(y)必须始终返回相同结果; - 非空性 :对于任何非空引用值
x,x.equals(null)必须返回false。
这五大规范是 equals() 方法的底层契约,违反任意一条,都会导致程序逻辑混乱。
1.2 hashCode () 方法的官方定义与默认实现
官方源码
java
public native int hashCode();
核心解读
- 关键字
native:表示这是一个本地方法,底层由 C/C++ 实现,直接调用操作系统底层逻辑生成哈希值; - 默认实现 :根据对象的内存地址 生成一个
int类型的整数(哈希码); - 核心作用 :为对象提供一个唯一的标识,用于哈希集合(HashMap/HashSet/HashTable) 快速定位对象的存储位置。
官方规范(JavaDoc)
- 同一个对象,在程序执行期间,只要没有修改 ,多次调用
hashCode()必须返回相同的整数; - 如果两个对象调用
equals()方法返回true,那么这两个对象的hashCode()必须返回相同的整数; - 如果两个对象调用
equals()方法返回false,它们的hashCode()可以相同,也可以不同(但推荐不同,提升哈希表效率)。
二、核心对比:equals () 与 hashCode () 的本质区别
很多开发者分不清两个方法的作用,我们从设计目的、使用场景、计算逻辑、依赖关系、性能 五个维度做全方位对比。
| 对比维度 | equals() | hashCode() |
|---|---|---|
| 核心设计目的 | 精准判断两个对象的逻辑内容是否相等 | 为对象生成哈希值,实现快速分组 / 定位 |
| 底层依赖 | 依赖对象的属性值(重写后) | 依赖对象的内存地址(默认)/ 属性值(重写后) |
| 计算成本 | 高:需要逐字段比对对象属性 | 低:直接返回一个整数,无复杂计算 |
| 主要使用场景 | 所有需要判断对象相等的场景 | 仅用于哈希表(HashMap/HashSet 等) |
| 返回值类型 | boolean(true/false) |
int(32 位整数) |
| 是否必须重写 | 自定义对象需要逻辑判等时必须重写 | 只要重写 equals,必须同时重写 |
| 核心价值 | 保证对象比较的准确性 | 保证哈希表操作的高效性 |
通俗比喻(最易理解)
我们把哈希集合 比作一个大型图书馆:
- hashCode () = 图书分类号 图书馆根据分类号,把图书放到对应的书架(哈希桶)中。找书时,先看分类号,直接定位到对应书架,速度极快。
- equals () = 图书 ISBN 编码 同一个书架上可能有很多书(哈希冲突),需要通过 ISBN 编码精准核对,找到你想要的那一本,精准无误。
总结:
hashCode()负责快速缩小范围;equals()负责最终精准确认 。两者配合,才能让哈希集合同时拥有O (1) 时间复杂度的高效查询能力。
三、黄金契约:equals () 与 hashCode () 的三大绑定规则
这是本文最核心 的知识点,也是 Java 语言的底层强制契约 。任何违反契约的代码,都会导致哈希集合彻底失效。
契约 1:equals () 相等 → hashCode () 必须相等
如果两个对象通过 equals() 判断为逻辑相等 ,那么它们的哈希码必须完全相同。
✅ 正确逻辑:对象A.equals(对象B) = true ➡️ 对象A.hashCode() = 对象B.hashCode()
❌ 违反后果:哈希集合会把两个逻辑相等的对象 ,放到不同的哈希桶 中,导致数据重复存储、查找失败。
契约 2:hashCode () 相等 → equals () 不一定相等
如果两个对象的哈希码相同,它们的 equals() 结果可以是 true,也可以是 false。
这种现象叫做哈希冲突,是哈希表的正常现象。
- 哈希冲突时,对象会挂载到同一个哈希桶的链表 / 红黑树上;
- 最终通过
equals()区分是否为同一个对象。
契约 3:对象不变 → hashCode () 永远不变
同一个对象,在程序运行期间,只要属性没有修改,无论调用多少次 hashCode(),返回值必须保持不变。
❌ 违反后果:将对象存入 HashMap 后,修改对象属性 → 哈希码改变 → 再也无法找到该对象 → 内存泄漏、数据丢失。
四、致命问题:只重写 equals () 不重写 hashCode () 的灾难
这是 Java 开发中最经典、最隐蔽、最高发 的 Bug。我们通过实战代码演示后果,让你直观理解契约的重要性。
4.1 测试场景
自定义一个 User 类,仅重写 equals(),不重写 hashCode(),将对象存入 HashSet(底层基于 HashMap 实现)。
java
import java.util.HashSet;
// 自定义用户类
class User {
private int userId;
// 构造方法
public User(int userId) {
this.userId = userId;
}
// 仅重写 equals:根据userId判断逻辑相等
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return userId == user.userId;
}
// 【错误】没有重写hashCode()
}
// 测试类
public class HashTest {
public static void main(String[] args) {
User u1 = new User(1001);
User u2 = new User(1001);
// 1. 判断逻辑相等
System.out.println("u1.equals(u2) = " + u1.equals(u2)); // 输出:true
// 2. 存入HashSet
HashSet<User> set = new HashSet<>();
set.add(u1);
set.add(u2);
// 3. 查看集合大小
System.out.println("集合元素个数:" + set.size()); // 输出:2
}
}
4.2 运行结果与问题分析
- 预期结果:
u1和u2逻辑相等,HashSet 应该去重,集合大小为1; - 实际结果:集合大小为
2,重复存储了两个逻辑相同的对象。
4.3 根本原因
u1和u2重写了equals(),返回true;- 没有重写
hashCode(),使用 Object 类默认实现:根据内存地址生成哈希码; u1和u2是两个不同对象,内存地址不同 → 哈希码不同;- HashSet 根据哈希码将两个对象放到不同的哈希桶中;
- HashSet 认为两个对象是不同元素,允许重复存储。
4.4 解决方案:同时重写两个方法
修改 User 类,必须同时重写 equals() 和 hashCode():
java
import java.util.Objects;
class User {
private int userId;
public User(int userId) {
this.userId = userId;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return userId == user.userId;
}
// 【正确】重写hashCode,使用与equals相同的属性
@Override
public int hashCode() {
return Objects.hash(userId);
}
}
再次运行测试代码:
u1.equals(u2) = trueu1.hashCode() = u2.hashCode()- 两个对象被放入同一个哈希桶
- HashSet 去重成功,集合大小 = 1
五、源码深度解析:HashMap 如何依赖 equals () 和 hashCode ()?
HashMap 是 Java 中最常用的集合,其核心逻辑完全依赖 hashCode() 和 equals()。我们通过 JDK 1.8 源码,解析两个方法的工作流程。
5.1 HashMap 存储 / 查找核心流程
- 调用
key.hashCode()计算哈希值 → 计算数组下标 → 定位哈希桶; - 如果哈希桶为空,直接存储对象;
- 如果哈希桶不为空,遍历链表 / 红黑树;
- 调用
key.equals()精准匹配,找到则覆盖,未找到则新增节点。
5.2 JDK 1.8 HashMap 核心源码(简化版)
1. 计算哈希值
java
static final int hash(Object key) {
int h;
// 调用key的hashCode()方法
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2. 查找元素(get 方法)
java
public V get(Object key) {
Node<K,V> e;
// 1. 计算hash值 2. 定位节点
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V> first;
// 根据hash值定位哈希桶
if ((first = tab[(n-1) & hash]) != null) {
// 【第一步】比较hashCode
if (first.hash == hash &&
// 【第二步】比较equals
(key == first.key || (key != null && key.equals(first.key))))
return first;
}
return null;
}
5.3 源码核心结论
- hashCode () 决定对象存在哪个桶;
- equals () 决定桶中哪个是目标对象;
- 缺少任意一个方法,或违反契约,HashMap 都会出现逻辑错误。
六、最佳实践:如何正确重写 equals () 和 hashCode ()?
在实际开发中,自定义类必须同时重写 两个方法,且遵循统一规范。
6.1 重写核心原则
- 两个方法必须使用完全相同的属性;
- 不要使用可变属性参与计算(避免哈希码改变);
- 优先使用 JDK 提供的
Objects工具类,避免空指针; - 直接使用 IDE 自动生成(推荐,无手写错误)。
6.2 IDEA 一键生成教程
- 定义类的属性;
- 快捷键
Alt + Insert; - 选择
equals() and hashCode(); - 勾选参与比较的属性 → 自动生成标准代码。
6.3 标准重写模板(通用版)
java
import java.util.Objects;
public class Student {
// 成员变量
private String name;
private int age;
private String studentId;
// 构造方法
public Student(String name, int age, String studentId) {
this.name = name;
this.age = age;
this.studentId = studentId;
}
// 重写equals()
@Override
public boolean equals(Object o) {
// 1. 自反性:同一对象直接返回true
if (this == o) return true;
// 2. 非空性+类型判断
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型强转
Student student = (Student) o;
// 4. 比较所有关键属性
return age == student.age &&
Objects.equals(name, student.name) &&
Objects.equals(studentId, student.studentId);
}
// 重写hashCode():使用与equals完全相同的属性
@Override
public int hashCode() {
return Objects.hash(name, age, studentId);
}
}
6.4 关键工具类:Objects
JDK 1.7 提供的 Objects 类,是重写两个方法的神器:
Objects.equals(a, b):自动处理空指针,安全比较对象;Objects.hash(a, b, c):自动组合多个属性生成哈希码,一行代码搞定。
七、进阶知识点:equals () 与 == 的区别
这是面试必考题,也是开发者最容易混淆的点。
7.1 核心区别
-
==:- 基本数据类型:比较值是否相等;
- 引用数据类型:比较内存地址是否相等。
-
equals():- 引用数据类型:默认比较内存地址,重写后比较对象内容。
7.2 实战代码演示
java
public class EqualsTest {
public static void main(String[] args) {
// 基本类型
int a = 10;
int b = 10;
System.out.println(a == b); // true
// 引用类型:String(重写了equals)
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2); // false(地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
// 自定义对象
Student stu1 = new Student("张三", 18, "001");
Student stu2 = new Student("张三", 18, "001");
System.out.println(stu1 == stu2); // false
System.out.println(stu1.equals(stu2)); // true
}
}
八、高频面试题全解(背会直接拿分)
面试题 1:equals () 和 hashCode () 的作用是什么?
equals():判断对象逻辑内容是否相等,重写后保证比较准确性;hashCode():生成对象哈希码,用于哈希表快速定位,保证操作效率。
面试题 2:为什么重写 equals () 必须重写 hashCode ()?
为了遵守 Java 官方契约,保证哈希集合(HashMap/HashSet)正常工作。如果只重写 equals,会导致逻辑相等的对象哈希码不同,出现数据重复、查找失败。
面试题 3:两个对象 equals 相等,hashCode 一定相等吗?
一定相等,这是 Java 强制契约。
面试题 4:两个对象 hashCode 相等,equals 一定相等吗?
不一定,这是哈希冲突,需要 equals 进一步精准判断。
面试题 5:hashCode () 是唯一的吗?
不是。不同对象可能生成相同的哈希码(哈希冲突)。
面试题 6:String 类重写 equals () 和 hashCode () 了吗?
重写了:
equals():比较字符串内容;hashCode():根据字符串字符计算哈希码。这也是 String 适合作为 HashMap key 的原因。
九、常见误区与避坑指南
误区 1:使用可变属性参与 hashCode () 计算
❌ 错误:修改属性 → 哈希码改变 → 对象丢失;✅ 正确:使用不可变属性(如 id、uuid)参与计算。
误区 2:重写 equals 时不遵守五大规范
❌ 错误:破坏对称性、传递性,导致程序逻辑混乱;✅ 正确:使用 IDE 自动生成,保证规范。
误区 3:认为 hashCode () 是唯一标识
❌ 错误:哈希码允许重复(哈希冲突);✅ 正确:哈希码仅用于快速定位,最终判断必须用 equals。
误区 4:null 对象调用 equals ()
❌ 错误:空指针异常;✅ 正确:使用 Objects.equals() 安全比较。
十、总结:万字长文核心提炼
- 底层定位 :
equals()和hashCode()是 Object 类的核心方法,是哈希集合的基石; - 核心分工 :
equals():精准判等,保证数据准确性;hashCode():快速定位,保证操作高效性;
- 黄金契约 :
- equals 相等 ➡️ hashCode 必相等;
- hashCode 相等 ➡️ equals 不一定相等;
- 开发铁律 :重写 equals 必须同时重写 hashCode,且使用相同属性;
- 实战技巧 :使用 IDE 自动生成 +
Objects工具类,避免手写错误; - 面试核心:理解契约、源码逻辑、区别对比,轻松应对所有相关面试题。
掌握 equals() 和 hashCode(),不仅能写出无 Bug 的 Java 代码,更能理解哈希表的底层设计思想,这是从初级开发者进阶为中高级开发者的必经之路。

总结
以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!