大家好,这里是后端基础拾光集。
不知道你有没有遇到过这种诡异线上 bug: 两个对象业务字段完全一模一样,放到 HashSet 里面,居然存成两条记录。 HashMap 查询的时候,明明参数一样,就是查不到数据。 很多人排查半天,最后发现:实体类只重写了 equals,没有重写 hashCode。 今天我们用身份档案类比,一次性吃透契约规则,面试必考。
equals 和 hashCode 生活化类比
我们可以把对象想象成一个人的身份档案。
equals:核对完整身份信息,判断是不是同一个人hashCode:分配楼栋房间号
核心契约就两条,超好记:
- 如果两个人是同一个人(
equals=true),必须分到同一个房间(hashCode 相等) - 同一个房间,可以住多个人(hashCode 相同,equals 不一定相等,哈希碰撞)
一句话记忆:同一个人,房间号一定相同;同一个房间,不一定是同一个人。
先看一个翻车现场
public class User {
private String name;
private int age;
// 构造方法、getter、setter 省略
}
// 测试
User u1 = new User("张三", 18);
User u2 = new User("张三", 18);
System.out.println(u1.equals(u2)); // false
明明是同一个人,结果 equals 返回 false?
因为 Object 默认的 equals 比较的是内存地址,两个 new 出来的对象地址肯定不同。
所以,只要你的对象需要"按内容比较",就必须重写 equals。
那 hashCode 呢?为什么也要一起重写?
equals 和 hashCode 的"契约"
Java 定了一条规矩,你必须遵守:
如果两个对象 equals 相等,那么它们的 hashCode 必须相等。
反过来不要求:hashCode 相等,equals 可以不等(这叫哈希冲突)。
如果你只重写 equals 不重写 hashCode,会发生什么?
java
Set<User> set = new HashSet<>();
set.add(new User("张三", 18));
set.contains(new User("张三", 18)); // false!
逻辑上明明是同一个对象,HashSet 却找不到。因为 HashSet 先用 hashCode 定位,再用 equals 比较。hashCode 不同,直接就被分到不同的桶里了,压根没机会走到 equals。
Object 原生默认行为
如果我们不重写这两个方法:
原生equals直接用==,对比对象的内存地址,只看是不是同一个档案原件。 原生hashCode,根据对象内存地址生成房间号。
如果只重写 equals,不改 hashCode:
两份档案信息完全一样,equals 判定是同一个人,但是系统分配了两个不同房间号。 HashMap、HashSet 就会认为是两个不同对象,直接存两份,bug 就此诞生。
结论:重写 equals 必须重写 hashCode,这是铁律。
怎么重写?最爱踩的坑
AI 生成实体类的时候,经常只生成 equals,忘记 hashCode。
// ❌ AI容易生成的错误代码
class User{
private Integer id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
// 缺少重写 hashCode()!
}
创建两个 id 相同的 User 对象,放入 HashSet,集合里面会存在两条数据,不符合预期。
✅正确写法
class User{
private Integer id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
关键要点:equals 和 hashCode,必须使用同一套字段。 不能 equals 拿 id 对比,hashCode 却用 name,直接破坏契约。
记住三步走:比引用 → 比类型 → 比字段。
更香的写法:Lombok
现在谁还手写啊,一个注解搞定:
@EqualsAndHashCode
public class User {
private Integer id;
private String name;
private int age;
}
或者直接用 @Data,它包含了 @EqualsAndHashCode。
但注意一个坑:Lombok 默认用所有字段生成,如果类有继承关系,要小心。子类加 @EqualsAndHashCode(callSuper = true) 才带上父类字段。
五、几个容易踩的坑
1. 用可变字段参与 hashCode
Set<User> set = new HashSet<>();
User u = new User("张三", 18);
set.add(u);
u.setName("李四"); // 改了字段
set.contains(u); // false!找不到了
对象放进 HashSet 后,别改参与 hashCode 的字段,否则等于"失联"。
2. String 的坑
Objects.equals(name, user.name) 别写成 name.equals(user.name),前者能防 null。
3. 只重写 equals 不重写 hashCode
IDE 会给你黄色警告,别无视它。
✅核心总结
- 契约:equals 相等,hashCode 一定相等;hashCode 相等,equals 不一定相等。
- 重写 equals,必须同步重写 hashCode,成对出现。
- equals 与 hashCode 必须选用完全相同的属性。
- 手写三步走,懒人用 Lombok
- 可变字段别参与,放进去就别改
📝本文属于专栏「后端基础拾光集」系列。 这个专栏结合生活化案例、线上踩坑实例讲解 Java 基础。 同系列已经更新:ArrayList&LinkedList 源码、HashMap、红黑树、Java 泛型、异常体系、Integer 缓存池。 专栏持续更新 Java 后端基础、源码、面试踩坑干货,欢迎点赞收藏关注,一起交流后端学习、面试踩坑经验。