hashCode 与 equals 的关系
面试官问:"hashCode 和 equals 有什么关系?"
候选人小赵答:"equals 相等,hashCode 必须相等。"
面试官追问:"反过来呢?hashCode 相等,equals 一定相等吗?"
小赵:"...应该不一定?"
面试官:"那两个对象 hashCode 相同、equals 为 false,会发生什么?"
小赵彻底卡住了。
【面试官心理】
我问这道题,是在测候选人对 HashMap 内部机制的理解。能说清楚"hash 碰撞"、"同桶链表"的,才是真正看过 HashMap 源码的候选人。
一、两大契约 🔴
Java 规范规定了 hashCode 与 equals 的核心契约:
契约一:equals 相等 → hashCode 必须相等
if (a.equals(b)) {
assert a.hashCode() == b.hashCode(); // 必须成立
}
契约二:hashCode 相等 → equals 不一定相等(允许 hash 碰撞)
if (a.hashCode() == b.hashCode()) {
// a.equals(b) 可能为 true,也可能为 false
}
这两条契约是 HashMap、HashSet 正确工作的基础。
二、HashMap 中的联动机制 🔴
2.1 put 流程中的 hashCode + equals
map.put(key, value);
// 1. 计算 key.hashCode(),确定桶位置
// 2. 如果桶为空,直接放入
// 3. 如果桶不为空(hash 碰撞),用 equals 逐一比较桶中已有 key
// - equals 相等:覆盖旧 value
// - equals 不等:追加到链表/红黑树
2.2 违反契约一的后果(equals 相等但 hashCode 不等)
class BadKey {
int id;
BadKey(int id) { this.id = id; }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BadKey)) return false;
return this.id == ((BadKey) obj).id;
}
// 没有重写 hashCode!用 Object 默认的(基于对象地址)
}
Map<BadKey, String> map = new HashMap<>();
BadKey k1 = new BadKey(1);
map.put(k1, "value");
BadKey k2 = new BadKey(1); // equals(k1) == true,但 hashCode 不同!
System.out.println(map.get(k2)); // null!查找失败
原因:k2.hashCode() 和 k1.hashCode() 不同(因为没有重写 hashCode),HashMap 去不同的桶找,当然找不到。
2.3 违反契约二不存在(不可能违反)
契约二说 hashCode 相等时 equals 不一定相等,这是哈希碰撞的正常现象。HashMap 通过链表/红黑树处理碰撞,不存在"违反"的说法。
2.4 hashCode 设计对性能的影响
// ❌ 极差的 hashCode:所有对象都返回同一个值
@Override
public int hashCode() { return 1; }
// 后果:HashMap 退化为链表,O(1) 查找变 O(n)
// ✅ 好的 hashCode:分布均匀,减少碰撞
@Override
public int hashCode() {
return Objects.hash(id, name); // 综合多个字段
}
⚠️
一个返回固定值的 hashCode 在技术上不违反契约,但会让 HashMap 完全失去哈希加速的优势,退化为 O(n) 查找。这是真实踩过的坑——有人为了"快速通过"测试,写了 return 1;,上线后 HashMap 性能崩溃。
三、标准实现模板 🔴
3.1 IDEA 自动生成
现代 Java 开发推荐使用 Objects.hash():
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User other = (User) obj;
return id == other.id && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name); // 自动处理 null,综合多个字段
}
3.2 高性能场景的 hashCode
// Effective Java 推荐的手动实现
@Override
public int hashCode() {
int result = Integer.hashCode(id); // 第一个字段
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + Integer.hashCode(age);
return result;
}
// 31 是质数,乘法散列效果好,且可以被编译器优化为位移:31*x = (x << 5) - x
💡
能说出"为什么用 31"的候选人,面试官会刮目相看:31 是质数,乘法散列效果好,同时 31 * x = (x << 5) - x 可以被 JIT 优化为位运算,性能更好。
四、追问升级
面试官:"不可变对象(如 String)的 hashCode 为什么可以缓存?"
回答:因为不可变对象的字段不会改变,hashCode 永远不会变。String 在第一次调用 hashCode() 时计算并缓存到 hash 字段,后续直接返回缓存值。
// String.hashCode() 源码
private int hash; // 缓存
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
for (char c : value) h = 31 * h + c;
hash = h;
}
return h;
}
面试官:"HashMap 的 key 必须是不可变的吗?"
回答:不是必须,但强烈建议。如果 key 是可变对象,put 后修改了 key 的字段,hashCode 变了,再用 get 会找到不同的桶,导致查找失败(value"丢失")。String 作为 key 的最佳实践正是利用了其不可变性。
【面试官心理】
能说出"可变 key 导致 HashMap 查找失效"的候选人,基本上真正理解了 hashCode 在 HashMap 中的作用。这个点很少有人主动提,能说出来直接加分。
五、生产避坑
5.1 使用 Lombok 时的陷阱
@Data // 自动生成 equals 和 hashCode
class User {
int id;
String name;
List<Order> orders; // 包含集合字段!
}
问题:@Data 生成的 hashCode 会包含 orders 集合,集合内容变化会导致 hashCode 变化。如果 User 被用作 HashMap 的 key,修改 orders 后 key "失效"。
解决:用 @EqualsAndHashCode(of = {"id"}) 只用稳定字段计算。
5.2 父子类的 equals 问题
class Point {
int x, y;
@Override public boolean equals(Object obj) {
if (!(obj instanceof Point)) return false;
Point p = (Point) obj;
return x == p.x && y == p.y;
}
}
class ColorPoint extends Point {
String color;
@Override public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) return false;
ColorPoint cp = (ColorPoint) obj;
return super.equals(cp) && color.equals(cp.color);
}
}
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, "red");
p.equals(cp); // true(Point 不检查 color)
cp.equals(p); // false(ColorPoint 要求 obj 是 ColorPoint 实例)
// 违反了 equals 的对称性!
这是 Effective Java 中讲到的经典问题,解决方案是用组合代替继承,或者在父类 equals 中加 getClass() 检查(但这样又破坏了里氏替换原则)。
⚠️
继承关系中正确实现 equals 是极其困难的。Java 规范承认这一点,推荐用组合代替继承,或者把父类设计为不可实例化的抽象类。