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 规范承认这一点,推荐用组合代替继承,或者把父类设计为不可实例化的抽象类。