深蓝黑背景品牌封面:左侧大标题「重写 equals 为什么必须重写 hashCode」,副标题「哈希容器里的两步定位」;右侧原创装饰——一排哈希桶,两个内容相同的人形徽章(标注 equals 相等)却落进了不同的桶,下方一个放大镜在错误的桶里找不到它;左上标注「经典题解析 · Java 后端」,右上标注「Day 03 · 10-11」 图 1:本文封面,两个「相等」的对象落进不同的桶,就是故障现场。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。equals 与 hashCode 是 Java 面试的钉子户,几乎人人会背「两个要一起重写」,但能讲清「不一起重写会在哈希容器里炸出什么故障」的人少得多。

想象面试现场:「谈谈你对 Object 里 equals 和 hashCode 的理解,什么场景需要重写这两个方法?」

第一步,先讲两个方法各自的默认语义。

Object 的默认 equals 是引用相等:x.equals(y) 等价于 x == y,只有两个引用指向同一个对象才算相等。默认 hashCode 则尽量让不同对象得到不同的整数。业务里我们常说的「相等」往往是值相等——两个用户对象,只要 ID 相同就认为是同一个人。所以重写 equals 的本质,是把「相等」的定义从「同一个对象」改成「同一份内容」。

第二步,讲清两者的约定。

JDK 官方文档给 hashCode 定了三条契约:同一个对象多次调用,只要参与 equals 比较的信息没变,结果必须不变;两个对象 equals 相等,hashCode 必须相等;equals 不等的对象不强制 hashCode 不同,但让它们尽量不同能改善哈希表性能。文档里还有一句直接回答题面的 API 说明:重写 equals 时通常必须同时重写 hashCode,否则就违反了「相等对象必须有相等哈希码」这条契约。

第三步,讲清违反约定会出什么故障:哈希容器的两步定位。

HashMap、HashSet 这类容器找元素是两步走的:第一步用 hashCode 算出桶下标,第二步只在这个桶里用 equals 逐个比内容。两步都通过,才算找到。

现在看故障链。假设你只重写了 equals 没重写 hashCode:两个内容相同的对象,hashCode 沿用默认实现,大概率不同。存入时,它们按各自的哈希码落进不同的桶,相安无事;查询时,容器先拿你手里这个对象的 hashCode 算桶号——去了一个错误的桶,然后在这个桶里用 equals 挨个比,永远轮不到真正相等的那个。外部表现就是那句让无数人 debug 到深夜的话:明明 put 过,get 出来却是 null;明明 add 过,contains 却返回 false。

class User {
    final long id;
    User(long id) { this.id = id; }
    // 只重写了 equals,没重写 hashCode
    @Override
    public boolean equals(Object o) {
        return o instanceof User u && u.id == this.id;
    }
}

Set<User> set = new HashSet<>();
set.add(new User(42));
System.out.println(set.contains(new User(42))); // false:两个对象落进了不同的桶

补上 @Override public int hashCode() { return Long.hashCode(id); },两个对象算到同一个桶,equals 才有机会执行,contains 才会返回 true。

第四步,讲清正确的重写姿势。

两条规则:一是字段集合必须一致——参与 equals 比较的字段,必须全部参与 hashCode 计算,一个不能多也不能少;二是优先用不可变字段——如果对象进集合后参与哈希的字段还会被改,它的桶号就变了,等于对象在容器里「失踪」,这是比不重写更隐蔽的坑。工程实践里,值对象、DTO、要进 Set 或当 Map key 的对象需要重写这对方法;一次性使用的临时对象不必。

机制与故障图:上部是约定条款——equals 相等则 hashCode 必须相等;hashCode 相等不要求 equals 相等(允许撞桶);equals 不等则 hashCode 最好不等(减少碰撞)。中部是哈希容器的两步定位——第一步用 hashCode 算出桶下标,第二步只在那个桶里用 equals 逐个比内容,两步都过才算找到。下部是故障链——只重写 equals 不重写 hashCode 时,两个内容相等的对象算出不同桶号,存入时各进各的桶,查询时先按 hashCode 找桶就去了错误的桶,equals 永远轮不到执行,表现为「明明存过却查不到」。底部结论:hashCode 决定去哪找,equals 决定找到后认不认,两个必须保持同一份「相等」的定义 图 2:约定条款、两步定位机制与违反后的故障链。

可能的追问。

「两个不相等的对象能有相同的 hashCode 吗?」当然能,这叫哈希碰撞,是正常现象——撞桶的对象在同一个桶里排队,靠 equals 区分。「反过来,hashCode 相同能说明 equals 相等吗?」不能,碰撞的存在让哈希码只能用来快速排除,不能用来确认相等。「hashCode 在判等里的角色到底是什么?」一句话:它负责快速缩小搜索范围,不负责判定相等。

易错点清单。

一,只会背「要一起重写」,讲不出两步定位的故障链。二,重写时字段集合不一致——equals 比了三个字段,hashCode 只用了两个。三,用可变字段算哈希,对象进集合后改字段导致「失踪」。四,以为哈希冲突是 bug,把「让不同对象哈希码都不同」当成必须满足的约束。

回到面试现场,建议的答题节奏:先讲默认语义与「值相等」的重写动机,再列三条契约,然后用两步定位拆解故障链——这是全题的分水岭,最后落到字段一致与不可变字段两条实践规则。这套约定在 AI 工程里天天兑现:特征对象、样本对象进 Set/Map 做去重与索引是日常操作,约定破坏带来的「明明存在却查不到」,是最难排查的隐蔽 bug 之一。

你遇到过 equals 或 hashCode 引发的离奇 bug 吗?最后是怎么定位到的?欢迎分享。