哈希碰撞就像 不同的人被分配到同一个储物柜(即不同的键被哈希到同一个数组位置)。HashMap 用两种方法解决碰撞:
一、核心方法:链表 + 红黑树
1. 链表法(Java 8 之前)
-
操作方式:
- 当多个键被哈希到同一个桶(数组位置)时,这些键值对会以链表形式存储。
- 插入新元素时,直接追加到链表末尾。
- 查找元素时,遍历链表逐个比较键的哈希值和内容(
equals())。
-
示例:
// 假设键 "A" 和键 "B" 哈希到同一位置 map.put("A", 1); map.put("B", 2); // 桶结构:["A=1" → "B=2"]
2. 红黑树优化(Java 8+)
-
触发条件:
- 当链表长度 ≥ 8,且数组容量 ≥ 64 时,链表转为红黑树。
- 当红黑树节点数 ≤ 6 时,退化为链表。
-
目的:
- 链表过长时查询慢(O(n)),转为红黑树(O(log n))提升效率。
-
示例:
// 插入8个哈希冲突的键 for (int i=0; i<8; i++) { map.put("Key" + i, i); } // 桶结构:红黑树(平衡二叉搜索树)
二、如何减少哈希碰撞?
-
优化哈希函数:
-
扰动算法:对键的
hashCode()进行高位运算,减少低位重复。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
自定义对象需重写
hashCode()和equals():class User { String name; int age; @Override public int hashCode() { return Objects.hash(name, age); // 合并多个字段的哈希值 } }
-
-
动态扩容:
- 当元素数量超过
容量 × 负载因子(默认0.75)时,数组扩容至2倍,重新分配键值对。 - 扩容后哈希冲突概率降低:哈希值计算时用更大的数组长度取模。
- 当元素数量超过
三、碰撞解决全流程
-
计算哈希值:通过扰动函数得到键的哈希值。
-
定位桶位置:
index = hash & (capacity - 1)。 -
处理碰撞:
- 若桶为空:直接插入新节点。
- 若桶为链表:遍历链表,若发现相同键则替换值;否则追加到链表尾部。
- 若桶为红黑树:按树结构插入或替换节点。
-
检查扩容或树化:
- 元素数量超阈值 → 扩容。
- 链表长度≥8且容量≥64 → 转红黑树。
四、总结
| 方法 | 优点 | 缺点 |
|---|---|---|
| 链表法 | 实现简单,小数据量高效 | 链表过长时查询慢(O(n)) |
| 红黑树法 | 大数据量查询快(O(log n)) | 树结构维护成本高 |
口诀:
「哈希碰撞不可怕,链表红黑来当家
扩容扰动降冲突,重写哈希是妙法
链表查询虽然慢,树化提速顶呱呱!」