只重写 equals 不写 hashCode?HashMap 直接出现诡异重复数据

0 阅读4分钟

大家好,这里是后端基础拾光集。

不知道你有没有遇到过这种诡异线上 bug: 两个对象业务字段完全一模一样,放到 HashSet 里面,居然存成两条记录。 HashMap 查询的时候,明明参数一样,就是查不到数据。 很多人排查半天,最后发现:实体类只重写了 equals,没有重写 hashCode。 今天我们用身份档案类比,一次性吃透契约规则,面试必考。

equals 和 hashCode 生活化类比

我们可以把对象想象成一个人的身份档案。

  • equals:核对完整身份信息,判断是不是同一个人
  • hashCode:分配楼栋房间号

核心契约就两条,超好记:

  1. 如果两个人是同一个人(equals=true),必须分到同一个房间(hashCode 相等)
  2. 同一个房间,可以住多个人(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 会给你黄色警告,别无视它。

✅核心总结

  1. 契约:equals 相等,hashCode 一定相等;hashCode 相等,equals 不一定相等。
  2. 重写 equals,必须同步重写 hashCode,成对出现。
  3. equals 与 hashCode 必须选用完全相同的属性。
  4. 手写三步走,懒人用 Lombok
  5. 可变字段别参与,放进去就别改

📝本文属于专栏「后端基础拾光集」系列。 这个专栏结合生活化案例、线上踩坑实例讲解 Java 基础。 同系列已经更新:ArrayList&LinkedList 源码、HashMap、红黑树、Java 泛型、异常体系、Integer 缓存池。 专栏持续更新 Java 后端基础、源码、面试踩坑干货,欢迎点赞收藏关注,一起交流后端学习、面试踩坑经验。