在 Java 集合里,`equals()` 和 `hashCode()` 看起来只是两个基础方法,真正出问题时却往往直接影响 `HashSet` 去重、`HashMap` 查找,甚至让重复数据和诡异 Bug 一路混进业务逻辑。本文不绕概念,先把必须遵守的规则讲透,再给出 JDK 7+ 的标准写法和 `User` 示例,最后结合 IDE 生成方式与常见误区,帮你判断哪些字段该参与比较、哪些字段绝对不能随手放进去。
先搞清楚:重写前必须记住的 4 条规则
重写这两个方法之前,先记住下面 4 条约束。它们不是编码习惯,而是保证哈希容器行为正确的前提。

- 两个对象 `equals` 为 `true`,`hashCode` 必须相同。这是硬性约束,破坏后集合行为会直接出错。
- `hashCode` 相同,`equals` 不一定为 `true`。哈希冲突本来就是允许存在的。
- 参与 `equals` 比较的属性,必须全部参与 `hashCode` 计算。少一个字段,就可能导致去重和查找不可靠。
- 只要对象内容没变,`hashCode` 就必须稳定。如果参与计算的是可变字段,对象放进集合后就可能“找不回来”。
一旦不遵守这些规则,`HashSet`、`HashMap` 这类容器就可能把本来应该视为同一个对象的数据当成不同元素处理,去重失败、查找异常、排查成本都很高。
JDK 7+ 推荐写法:直接用 `Objects.equals` 和 `Objects.hash`
如果项目运行在 JDK 7 及以上版本,最常见也最稳妥的写法就是使用 `Objects.equals` 和 `Objects.hash`。这套方式可读性好,空值处理也更省心。
通用模板
下面这份模板可以直接套用,重点只有一个:`equals()` 里比较了哪些字段,`hashCode()` 就必须使用完全相同的字段。
import ja va.util.Objects;
public class 类名 {
// 你的成员变量
private 类型 属性1;
private 类型 属性2;
// ======================== 重写开始 ========================
@Override
public boolean equals(Object o) {
// 1. 同一个对象,直接返回 true
if (this == o) return true;
// 2. 为null 或 类型不同,返回 false
if (o == null || getClass() != o.getClass()) return false;
// 3. 强制类型转换
类名 其他对象 = (类名) o;
// 4. 比较【所有关键业务属性】(决定是否重复的字段)
return Objects.equals(属性1, 其他对象.属性1)
&& Objects.equals(属性2, 其他对象.属性2);
}
@Override
public int hashCode() {
// 必须和 equals 里的属性完全一致!!!
return Objects.hash(属性1, 属性2);
}
// ======================== 重写结束 ========================
}
这个模板里最值得注意的是 `getClass() != o.getClass()` 这一层类型判断。它保证了比较发生在同一具体类型之间,避免不同类型对象被误判为相等。
实战示例:`User` 对象该按哪些字段去重
真正落到业务里,难点通常不是“怎么写代码”,而是“哪些字段才算业务唯一”。下面用一个 `User` 类说明这个判断过程。
示例代码
假设当前场景里,`id + username` 才代表唯一用户,而 `age` 只是普通属性,不参与去重。
import ja va.util.Objects;
public class User {
private Long id;
private String username;
private Integer age; // 假设 age 不参与去重
// 构造、get、set 省略...
// ===================== 核心重写 =====================
@Override
public boolean equals(Object o) {
if (this == o) return true;
// 判断类型安全
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
// 只比较【业务唯一】的字段:id + username
return Objects.equals(id, user.id)
&& Objects.equals(username, user.username);
}
@Override
public int hashCode() {
// 必须和 equals 完全一致!!!
return Objects.hash(id, username);
}
}
放进 `HashSet` 后会发生什么
new User(1, "张三", 20)new User(1, "张三", 30)
这两个对象虽然 `age` 不同,但 `id` 和 `username` 一样,因此放入 `HashSet` 后会被视为重复元素,只保留一份。这说明:是否相等,取决于你选定的业务唯一字段,而不是对象里所有属性都必须参与比较。
IDE 自动生成可以用,但字段选择必须自己负责
IntelliJ IDEA 和 Eclipse 都支持一键生成 `equals()` 和 `hashCode()`,在日常开发里完全可以直接使用。
常见快捷键如下:
- Windows:`Alt + Insert`
- Mac:`Cmd + N`
进入生成菜单后,选择 equals() and hashCode(),再勾选参与去重的字段即可。生成出来的代码通常和上面的标准模板一致,问题不在“能不能生成”,而在于你是否知道该勾哪些字段。
这也是为什么很多项目里即便用了 IDE 自动生成,后续字段一变,集合行为还是会出错。原因往往不是工具不行,而是类结构调整后没有同步更新这两个方法。
最容易踩的 3 个坑
下面这 3 类问题最常见,而且大多都不是编译期报错,而是运行一段时间后才暴露。

错误 1:`equals` 和 `hashCode` 使用的字段不一致
// 错误示范 equals 用 id hashCode 用 username → 去重失效!
这是最隐蔽的一种错误。比如 `equals()` 认定两个对象相等,但 `hashCode()` 因为用的是别的字段,算出来的值不同,`HashSet` 在哈希阶段就把它们分到不同位置,后续甚至不会进入 `equals()` 比较。
错误 2:只重写 `equals()`,不重写 `hashCode()`
// 致命错误 // HashSet 会先判断 hashCode,不同就直接插入,根本不会走 equals!
这类问题非常典型。因为默认 `hashCode()` 往往基于对象地址或对象标识计算,不同实例即使内容完全一样,哈希值也可能不同。结果就是你明明定义了“内容相等”,但哈希容器根本不按这个规则工作。
错误 3:把可变字段放进 `hashCode()`
如果你把后续会修改的字段,例如 `name`、`status` 这类值,拿去参与 `hashCode()` 计算,那么对象放入 `HashSet` 或作为 `HashMap` 键之后,只要字段发生变化,哈希位置也会随之变化。
这时常见的结果是:对象明明还在集合里,却查不到、删不掉,或者缓存命中异常。遇到这类问题时,排查难度通常很高,因为代码表面上看没有报错,问题出在对象进入容器前后的状态不一致。
最后记一遍:面试和开发里真正要抓住的结论
- `equals()` 决定的是业务语义上的“是否相等”。
- `hashCode()` 决定的是对象在哈希结构里的定位效率。
- `HashSet` 去重依赖两者共同生效,不是只看其中一个。
- 重写时最核心的原则,就是字段保持一致:参与 `equals()` 的属性,必须全部参与 `hashCode()`。







