为什么哈希表的 key 必须满足 equals 和 hashCode 契约?
简化版
哈希表要求“相等的 key 必须有相同的哈希值”。如果 equals 判等相同但 hashCode 不同,同一个逻辑 key 可能落到不同桶里,导致查不到、重复插入或删除失败。
详细版
哈希表先用 hash 定位桶,再在桶内用相等判断确认 key。这个两阶段过程要求:
- 如果
a.equals(b) == true,那么a.hashCode() == b.hashCode()必须为 true。 - 如果 hashCode 相同,equals 不一定相同,只是发生哈希冲突。
- 作为 key 的字段最好不可变,否则插入后 hash 变化会导致定位失败。
- 重写 equals 时通常必须同步重写 hashCode。
面试回答重点是:hash 负责“去哪找”,equals 负责“是不是它”。两者契约破坏后,哈希表的查找路径就断了。
完整版教学
一、哈希表查找为什么分两步
哈希表不会把所有 key 都逐个比较。它先根据 key 的哈希值计算桶位置,再只在这个桶或探测序列里找目标。
key -> hashCode -> bucketIndex -> equals 比较确认
这意味着 hashCode 决定“候选范围”,equals 决定“最终身份”。如果两个逻辑相等的 key 进入了不同候选范围,后续 equals 根本没有机会执行,查找就会失败。
二、核心契约到底是什么
核心契约不是“hashCode 不同,equals 一定不同”,而是反过来:
如果 equals 为 true,则 hashCode 必须相同。
允许两个不相等对象有相同 hashCode,因为哈希空间有限,冲突不可避免。哈希表本来就会用 equals 处理冲突。
| 关系 | 是否允许 | 结果 |
|---|---|---|
| equals 相同,hashCode 相同 | 允许且必须 | 正常查找 |
| equals 不同,hashCode 相同 | 允许 | 冲突,桶内继续比较 |
| equals 相同,hashCode 不同 | 不允许 | 可能查不到 |
| equals 不同,hashCode 不同 | 允许 | 常见情况 |
三、破坏契约会发生什么
假设 User(id=1) 的 equals 只看 id,但 hashCode 使用对象地址。两次创建的 User(1) equals 相同,hashCode 不同。
map.put(new User(1), "Alice");
map.get(new User(1)); // 可能返回 null
原因不是值不存在,而是第二个对象根据自己的 hash 找到了另一个桶。哈希表不会全表扫描,所以它看不到第一个桶里的那个逻辑相等 key。
四、可变 key 为什么危险
插入后如果 key 的参与 hash 的字段被修改,key 会“住在旧桶里,却应该被新 hash 找到”。
插入时:id=1 -> bucket 3
修改后:id=2 -> bucket 8
get(key) 会去 bucket 8,但对象还在 bucket 3
这类 bug 很隐蔽,因为对象引用还在,打印也能看到字段变了,但哈希表内部位置不会自动迁移。作为 key 的字段应该保持不可变。
五、hashCode 质量和契约不是一回事
契约要求正确性,质量影响性能。一个合法但很差的 hashCode 可以永远返回 1,它满足“相等对象 hash 相同”,但所有 key 都进同一个桶。
hashCode() { return 1; }
这样查找会从平均 O(1) 退化,具体取决于冲突处理结构。面试时要区分:契约破坏会错,哈希质量差会慢。
六、工程上怎么写更稳
如果 key 由多个字段组成,equals 和 hashCode 应使用同一组字段。比如只用 userId 判等,就只用 userId 参与 hash;如果 tenantId + userId 才唯一,两者都要参与。
记忆钩子:hashCode 决定“去哪个桶找”,equals 决定“桶里哪个是它”;相等对象 hash 不同,就是把目标藏到另一条路上。
七、常见误区与追问
- 误区:hashCode 相同就说明两个对象相等。 不对,hash 冲突允许存在,最终还要 equals 判断。
- 误区:只重写 equals 不重写 hashCode 也没事。 在哈希表中会导致相等对象可能落到不同桶,查找失败。
- 误区:key 放进 map 后字段可以随便改。 如果字段参与 hash 或 equals,修改会破坏定位路径。
- 追问:hashCode 永远返回 1 可以吗? 正确性可能没问题,但冲突极多,性能会明显变差。
- 追问:为什么 String 适合做 key? 字符串不可变,equals/hashCode 稳定,且 hash 实现质量通常较好。
八、加强记忆
这题抓住“先 hash 定位,再 equals 确认”就不会乱。equals/hashCode 契约保障的是哈希表能找到候选桶;不可变 key 保障的是插入后路径不变;好的 hash 分布保障的是候选桶不要太拥挤。正确性、稳定性、性能是三层不同要求。