Java 基础-1: 为什么重写对象的 equals 方法,必须同时重写 hashCode 方法?

32 阅读4分钟

在 Java 编程中,equals()hashCode() 是两个看似简单却极易被忽视其深层联系的方法。许多开发者在自定义类时会重写 equals() 以实现基于业务逻辑的对象相等判断,却常常忘记同步重写 hashCode()。这种疏忽看似无害,实则可能在使用 HashMapHashSet 等哈希结构时埋下难以察觉的“定时炸弹”。

本文将深入剖析:为什么一旦重写了 equals(),就必须重写 hashCode()?不这么做会带来哪些问题?它是否会影响对象序列化?


一、Java 的契约:equals 与 hashCode 的绑定关系

Java 官方文档(Object 类)明确规定了 equals()hashCode() 必须遵守的一致性契约

如果两个对象通过 equals() 判断为相等,那么它们的 hashCode() 必须返回相同的整数值。

换句话说:

a.equals(b) == true  ⇒  a.hashCode() == b.hashCode()

这个规则不是建议,而是强制要求。违反它,就等于破坏了 Java 集合框架的基础假设。


二、不重写 hashCode 会引发什么问题?

1. HashMap 中“找不到”已存在的 key

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30); // 内容相同,不同实例

map.put(p1, "value");
System.out.println(map.get(p2)); // 期望 "value",实际可能为 null!

原因
HashMap.get(key) 首先根据 key.hashCode() 定位桶(bucket)。如果两个逻辑相等的对象因未重写 hashCode() 而哈希值不同,它们会被分配到不同的桶中,equals() 根本不会被调用——结果就是“查无此 key”。


2. HashSet 中出现重复元素

Set<Person> set = new HashSet<>();
set.add(new Person("Bob", 25));
set.add(new Person("Bob", 25));

System.out.println(set.size()); // 输出 2,而不是预期的 1!

原因
HashSet 底层依赖 HashMap,同样先比对哈希码。哈希码不同 → 直接视为不同元素 → 违反 Set “无重复” 的语义。


3. 程序行为不可预测,难以调试

默认的 Object.hashCode() 通常基于对象内存地址生成。这意味着:

  • 同一个逻辑对象,在不同 JVM 实例或运行周期中,哈希码可能不同;
  • Bug 表现具有偶发性,在测试环境正常,上线后却频繁出错;
  • 尤其在缓存、分布式系统中,这类问题极难复现和定位。

三、与对象序列化的关联

你可能会问:这会影响序列化吗?

答案是:间接影响,但影响严重。

Java 的标准序列化机制(Serializable)本身不调用也不保存 hashCode(),只保存字段值。因此,单个对象的序列化/反序列化不受影响。

但是! 如果你的对象被用作 HashMapHashSet 的 key/元素,问题就来了:

// 序列化前
Set<Person> set = new HashSet<>();
set.add(new Person("Alice", 30));

// 反序列化后
Set<Person> restored = deserialize();
System.out.println(restored.contains(new Person("Alice", 30))); // ❌ 可能返回 false!

原因
反序列化会重建对象,若未重写 hashCode(),新对象的哈希码与原始对象不同(因内存地址变化),导致集合无法识别“相等”对象。

结论:只要对象可能被放入哈希集合并参与序列化,就必须正确重写 hashCode()


四、正确做法:成对重写

任何时候重写 equals(),都应同步重写 hashCode(),且两者必须基于相同的字段

推荐使用 Objects.hash() 工具方法:

@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);
}

@Override
public int hashCode() {
    return Objects.hash(name, age); // 与 equals 中使用的字段一致
}

或者使用 Lombok 注解(更简洁安全):

@EqualsAndHashCode
public class Person {
    private String name;
    private int age;
}

五、总结

场景是否需重写 hashCode
未重写 equals❌ 不需要
重写了 equals必须重写
对象用作 Map key / Set 元素✅ 强烈建议
对象可序列化且用于集合✅ 必须重写

黄金法则
“重写 equals 而不重写 hashCode,等于在代码中埋雷。”

理解并遵守这一基础契约,不仅能避免诡异的集合行为,还能提升程序的健壮性与可维护性。这是每一位 Java 开发者都应掌握的核心常识。


延伸思考

  • 如果 hashCode() 返回常量(如 return 1;),虽然满足契约,但会导致哈希表退化为链表,性能急剧下降。
  • 在不可变对象中,可考虑缓存 hashCode 值以提升性能。

保持对基础细节的敬畏,才能写出真正可靠的代码。

作者:Beata - 后端服务架构自由人
最后更新:2026 年 01 月 17 日
版权声明:本文可自由转载,注明出处即可。