在 Java 的 HashMap
中,当不同的键(key)经过哈希计算后映射到底层数组的同一个位置(即发生了哈希冲突 )时,采用链表 作为首要解决方案,将所有这些冲突的键值对连接起来存储在该位置,这个链表结构由 Node
对象构成 。
🔗 链表(Node)如何工作
HashMap
底层维护了一个 Node<K,V>[]
数组。你可以将数组的每个位置想象成一个"桶"(bucket) 。每个 Node
对象不仅存储着键(key
)、值(value
)和预先计算好的哈希值(hash
),还包含一个至关重要的引用 next
,它指向下一个 Node
节点 。
当发生哈希冲突,即两个不同的键被计算到同一个数组索引时,HashMap
的处理流程如下:
-
检查首个节点:首先检查目标桶是否为空。如果为空,则直接创建新节点放入。
-
处理冲突 :如果该桶不为空(已有节点),说明发生了冲突。这时,
HashMap
会遍历该桶上的链表,寻找是否已存在相同的键(通过equals
方法判断) 。 -
追加或更新:
- 键已存在:如果找到相同的键,则用新的值替换掉旧值。
- 新键 :如果没有找到相同的键,则会创建一个新的
Node
对象,并通过修改指针,将其添加到链表的末尾(在JDK 1.8之后采用尾插法) 。
这个过程可以直观地理解为:数组的每个槽位都挂载着一条链表,所有哈希值冲突的键值对都按顺序存放在这条链表中 。
⚖️ 链表的优化与权衡
使用链表解决冲突是一种经典且直观的方法,但它有其固有的优缺点。
-
优点:
- 实现简单:逻辑清晰,易于理解和实现 。
- 高效插入删除:在已知链表头节点的情况下,插入和删除新节点的操作非常高效,时间复杂度接近O(1) 。
-
缺点:
- 查询性能瓶颈:在最坏情况下,如果大量键都冲突到同一个桶中,会导致链表变得非常长。此时查询效率会从理想的O(1)退化为O(n),因为需要从头到尾遍历整个链表 。
🌳 从链表到红黑树的进化
为了解决长链表导致的性能下降问题,JDK 8 对 HashMap
进行了一项重要优化:当链表的长度超过一个阈值(默认为 8 ),并且当前整个哈希表的容量(数组长度)也达到一定值(默认为 64 )时,HashMap
会自动将这条链表转换为一棵红黑树 (TreeNode
) 。
为什么要转换?
红黑树是一种自平衡的二叉搜索树,其最大的优势在于能够将查询、插入和删除操作的时间复杂度维持在 O(log n)。即使数据量很大,性能衰减也比O(n)的链表要平缓得多。当链表较长时,将其转为红黑树可以显著提升在严重哈希冲突情况下的查询效率 。
转换条件的意义:
- 链表长度阈值 (8):这是一个基于统计学概率的权衡。在理想的哈希函数下,链表长度达到8的概率已经非常低,此时转为树结构带来的性能收益大于维护树结构的额外开销 。
- 最小容量阈值 (64) :如果哈希表本身容量很小,优先通过扩容(
resize
)来减少冲突可能是更合理的选择,因为扩容也能有效分散元素。设置这个条件避免了在小表情况下不必要的、相对昂贵的树化操作 。
当红黑树中的节点数量由于删除操作减少到另一个阈值(默认为 6)时,为了节省空间,它又会退化为链表 。
💎 总结
总而言之,链表是 HashMap
解决哈希冲突的基础且核心的机制 。它通过 Node
节点的 next
指针将冲突的元素串联起来。而 JDK 8 引入的链表与红黑树相互转换 的机制,则是一种智能的优化,旨在不同数据分布下动态调整数据结构,从而在时间和空间成本之间取得最佳平衡,确保 HashMap
在绝大多数场景下都能保持高效 。