equals 与 hashCode
深入理解 equals/hashCode 契约及在集合中的影响
equals 与 hashCode
如果你把对象当 HashMap 的 key 来用,却没正确重写 equals 和 hashCode——那要么 key 永远找不到,要么同样的值存了好几份。这两个方法是 HashMap/HashSet 的地基。
学完本章你将: 理解 equals 契约、hashCode 原理、为什么两者必须一起重写。
HashMap 是怎么工作的(为什么需要 hashCode)
put("Alice", 25):
1. 计算 "Alice".hashCode() → 比如 12345
2. 12345 % 数组长度 → 确定了放在哪个"桶"
3. 放入该桶
get("Alice"):
1. 计算 "Alice".hashCode() → 12345
2. 找到对应桶
3. 用 equals() 逐条比对桶里的 key,找到 Alice
所以:hashCode 决定"去哪个桶找",equals 决定"桶里的哪个是你"。缺一不可。
equals 契约 —— 五项铁律
java
public class User {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 同一个引用,肯定相等
if (!(o instanceof User u)) return false; // 2. 类型不对,肯定不等
return age == u.age && Objects.equals(name, u.name);
}
}
必须要遵守的五条规则:
| 原则 | 含义 | 示例 |
|---|---|---|
| 自反性 | `x.equals(x)` 永远 true | 自己和自己的副本 |
| 对称性 | `a.equals(b)` == `b.equals(a)` | 不能因为你是我朋友,我就不是你朋友 |
| 传递性 | `a.equals(b) && b.equals(c)` → `a.equals(c)` | 继承关系中容易违反 |
| 一致性 | 多次调用结果不变 | 除非数据被修改 |
| null 安全 | `x.equals(null)` 永远 false | 不要抛 NPE |
hashCode 契约
java
@Override
public int hashCode() {
return Objects.hash(name, age); // JDK 7+,自动算
}
三条铁律:
- equals 相等的两个对象,hashCode 必须相等(HashMap 把他们当同一个 key)
- hashCode 相等的两个对象,不一定 equals 相等(hash 碰撞,正常现象)
- hashCode 不等的两个对象,equals 一定不等
不重写 hashCode 的经典 bug
java
class BadKey {
String name;
// 只重写 equals,没重写 hashCode!
@Override public boolean equals(Object o) { /* 正确的 */ }
}
Map<BadKey, String> map = new HashMap<>();
BadKey k1 = new BadKey("Alice");
map.put(k1, "数据");
BadKey k2 = new BadKey("Alice"); // 内容相同
System.out.println(map.get(k2)); // null!!!
原因:k1 和 k2 虽然 equals 相等,但 hashCode 不同(继承了 Object 的,基于内存地址),HashMap 去不同的桶里找——根本找不到。
⚠️ 黄金法则:重写 equals 必须重写 hashCode,重写 hashCode 必须重写 equals。
public int hashCode() { return Objects.hash(name, age); // JDK 7+ }