在 Java 中,HashMap 是基于哈希表(数组 + 链表 / 红黑树)实现的键值对存储结构。
哈希冲突指的是:不同的键(key)通过哈希函数计算后,得到的哈希值相同,导致需要存储在数组的同一个索引位置。
HashMap 解决哈希冲突的核心方案是 “链地址法”(拉链法),并结合了 “链表转红黑树”“扰动函数”“动态扩容” 等机制优化性能。
具体如下:
链地址法是 HashMap 解决哈希冲突的基础手段,原理是:
HashMap 底层维护一个数组(称为 “哈希桶”,table),数组的每个元素是一个链表(或红黑树)的头节点。key 计算出相同的数组索引时,它们会被存储在该索引位置的链表(或红黑树)中,形成 “拉链” 结构。例如:
key1 和 key2 哈希冲突(索引均为 i),则 table[i] 会指向一个链表,key1 和 key2 对应的键值对(Node)会依次挂载到这个链表上。
当同一个索引位置的链表过长时,查询效率会从 O(1) 退化到 O(n)(链表遍历)。为解决这个问题,JDK 1.8 引入了红黑树优化:
HashMap 会将该链表转换为红黑树(一种自平衡二叉查找树)。O(log n),远高于长链表的 O(n),保证了哈希冲突严重时的性能。反之,当红黑树的节点数量减少到 6 个时,会重新退化为链表(因为少量节点时,链表的维护成本更低)。
哈希冲突的根源是 “不同 key 计算出相同索引”。HashMap 通过扰动函数(hash() 方法)优化哈希值的计算,减少冲突概率:
key 的哈希值首先调用 key.hashCode() 得到一个初始哈希值(int 类型,32 位)。
对初始哈希值进行 “扰动”(多次异或和右移),目的是让哈希值的高位与低位混合,增强哈希值的随机性,从而减少不同 key 计算出相同索引的概率。
JDK 1.8 的扰动函数实现:
static final int hash(Object key) {
int h;
// 关键:将哈希值的高位与低位异或,让高位参与索引计算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
通过扰动后的哈希值,与数组长度 n 进行 & 运算(等价于 hash % n,但更高效),得到最终的数组索引:
int index = (n - 1) & hash; // n 是数组长度(必须是 2 的幂次方)
当 HashMap 中的元素数量过多时,每个哈希桶(数组索引)会存储更多元素,冲突概率增加。HashMap 通过动态扩容缓解这个问题:
当元素数量(size)超过 负载因子(默认 0.75)× 数组长度(capacity) 时,触发扩容。
例如:数组长度为 16,负载因子 0.75,当元素数超过 12 时,触发扩容。
(n-1) & hash 计算索引的有效性)。扩容后,每个哈希桶存储的元素数量减少,哈希冲突的概率降低。
HashMap 解决哈希冲突的核心逻辑是:
这些机制共同作用,使 HashMap 在哈希冲突存在的情况下,仍能保持高效的增删改查性能(平均时间复杂度接近 O(1))。