图 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 的对象需要重写这对方法;一次性使用的临时对象不必。
图 2:约定条款、两步定位机制与违反后的故障链。
可能的追问。
「两个不相等的对象能有相同的 hashCode 吗?」当然能,这叫哈希碰撞,是正常现象——撞桶的对象在同一个桶里排队,靠 equals 区分。「反过来,hashCode 相同能说明 equals 相等吗?」不能,碰撞的存在让哈希码只能用来快速排除,不能用来确认相等。「hashCode 在判等里的角色到底是什么?」一句话:它负责快速缩小搜索范围,不负责判定相等。
易错点清单。
一,只会背「要一起重写」,讲不出两步定位的故障链。二,重写时字段集合不一致——equals 比了三个字段,hashCode 只用了两个。三,用可变字段算哈希,对象进集合后改字段导致「失踪」。四,以为哈希冲突是 bug,把「让不同对象哈希码都不同」当成必须满足的约束。
回到面试现场,建议的答题节奏:先讲默认语义与「值相等」的重写动机,再列三条契约,然后用两步定位拆解故障链——这是全题的分水岭,最后落到字段一致与不可变字段两条实践规则。这套约定在 AI 工程里天天兑现:特征对象、样本对象进 Set/Map 做去重与索引是日常操作,约定破坏带来的「明明存在却查不到」,是最难排查的隐蔽 bug 之一。
你遇到过 equals 或 hashCode 引发的离奇 bug 吗?最后是怎么定位到的?欢迎分享。