为什么重写 equals 时一定要重写 hashCode?
结论:因为 Object 的通用约定(general contract)要求——equals 判定相等的两个对象,hashCode 必须相等。这条约定是哈希容器(hash-based collection)正确性的前提:HashMap/HashSet 先用 hashCode 定位桶(bucket),再用 equals 在桶内比较。只重写 equals 会让"相等"的对象落到不同的桶里,导致 put 进去 get 不出来、Set 里出现"重复"元素。
1. 哈希容器怎么用这两个方法
map.put(key, value) map.get(key)
│ │
▼ ▼
hash = spread(key.hashCode()) hash = spread(key.hashCode())
│ │
▼ ▼
index = (n - 1) & hash index = (n - 1) & hash
│ │
▼ ▼
桶空?→ 直接放 遍历该桶链表/红黑树
桶非空 → 逐个比较: 对每个节点判断:
(k == key) || key.equals(k) (k == key) || key.equals(k)
│ │
▼ ▼
命中 → 覆盖 命中 → 返回 value,否则 null
关键点:hashCode 决定"去哪个桶找",equals 决定"桶里哪个是它"。两步缺一不可,如果 hashCode 不一致,equals 根本没有机会被调用。
2. 只重写 equals 会怎样
class OrderNo {
private final String value;
OrderNo(String value) { this.value = value; }
@Override public boolean equals(Object o) { // 只重写 equals
if (this == o) return true;
if (!(o instanceof OrderNo)) return false;
return value.equals(((OrderNo) o).value);
}
}
Set<OrderNo> set = new HashSet<>();
set.add(new OrderNo("A001"));
set.add(new OrderNo("A001"));
System.out.println(set.size()); // 2,重复元素"漏"进来了
Map<OrderNo, String> map = new HashMap<>();
map.put(new OrderNo("A001"), "已支付");
System.out.println(map.get(new OrderNo("A001"))); // null,取不出来
两次 new 的对象 hashCode 是 Object 给的标识哈希(identity hash),必然不同,于是进入两个不同的桶,equals 从未参与比较。
3. 四种组合的结果
| 重写情况 | equals 语义 | hashCode | 后果 |
|---|---|---|---|
| 都不重写 | 引用相等 | 标识哈希 | 只有同一对象才相等 |
只重写 equals | 内容相等 | 标识哈希 | 违反契约,哈希容器失效 |
只重写 hashCode | 引用相等 | 内容哈希 | 哈希相同但 equals 不等,仍不等 |
| 都重写 | 内容相等 | 内容哈希 | 正确 |
| 都重写但字段可变 | 内容相等 | 随字段变化 | key 的哈希改变,get 找不到(内存泄漏) |
4. 正确写法
import java.util.Objects;
public final class OrderNo {
private final String value;
public OrderNo(String value) { this.value = value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderNo)) return false;
OrderNo other = (OrderNo) o;
return value.equals(other.value);
}
@Override
public int hashCode() {
return Objects.hash(value); // 与 equals 使用同一组字段
}
}
三条纪律:
- 字段一致:
equals用哪些字段,hashCode就用哪些字段; - 不可变:作为 key 的字段必须是
final的,否则入表后哈希变化; - 对称与传递:
equals要满足自反、对称、传递、一致、非空五个约定,不要拿getClass()和instanceof混用。
如果是 Java 16+ 的 record,编译器自动生成基于全部组件的 equals/hashCode,无需手写;Lombok 的 @EqualsAndHashCode 也做同样的事,但要留意它默认包含全部非静态字段。
5. 常见追问
| 追问 | 回答要点 |
|---|---|
hashCode 相同,equals 一定相等吗? | 不一定,哈希冲突(collision)允许不同对象同哈希,所以桶内还要比 equals |
equals 相等,hashCode 一定相等吗? | 必须相等,否则契约被破坏 |
hashCode 返回常量 0 合法吗? | 合法,但所有元素挤进一个桶,退化成链表 O(n) |
为什么用 Objects.hash 而不是手写? | 简洁、可读、自动处理 null;代价是可变参数会创建数组,极致性能场景可手写 |
equals 用 instanceof 还是 getClass()? | 允许子类相等用 instanceof;要求严格同类型用 getClass(),继承体系里优先考虑 final 类 |
| 可变对象做 key 有什么后果? | 哈希改变后桶位置错乱,get 返回 null,且旧节点长期滞留形成逻辑泄漏 |
只重写 hashCode 行不行? | 不行,HashMap 桶内仍用 equals 比较,引用不等就作为新节点插入 |
小结
一句话:hashCode 负责"找得到",equals 负责"认得对"。重写时两者必须基于同一组字段、同时重写;作为 key 的对象要保持不可变。Objects.hash 是标准写法,record/Lombok 是省事写法。