【Android 、Java】为什么HashMap在JDK8中要将链表转换为红黑树的阈值设为8?这个数字是如何确定的?
HashMap 在 JDK8 中引入红黑树的主要目的是为了应对极端情况下的哈希碰撞,防止链表过长导致查询性能急剧下降。将链表转换为红黑树的阈值设定为 8,并非一个随意选择的值,而是基于概率统计、性能权衡和工程实践的综合考量。
下面这个表格可以帮助您快速理解其核心考量和背后的数学依据。
考量维度 具体说明
统计学基础 (泊松分布) 在理想的哈希函数下,HashMap 中单个桶的链表长度服从泊松分布。计算表明,链表长度达到 8 的概率约为 0.00000006 (千万分之六) 。这个概率极低,意味着一旦链表长度达到 8,说明哈希冲突已非常严重,此时转为红黑树是必要的兜底策略。
性能权衡 (时间 vs 空间) 红黑树的查询时间复杂度为 O(log n),优于链表的 O(n)。但树节点(TreeNode)占用的内存空间大约是普通链表节点(Node)的两倍。阈值设为8是在时间和空间之间取得的平衡:当链表足够长、树化的性能收益能覆盖其空间开销时,才进行转换。
避免频繁转换 (缓冲机制) 树化阈值是 8,但链化阈值(红黑树退化为链表的阈值)是 6。两者之间差值 2 形成了一个缓冲带。这可以有效防止在阈值附近频繁的增删操作导致链表和红黑树之间反复转换,避免不必要的性能开销。
🔢 深究泊松分布
阈值8的选择有着坚实的数学基础。HashMap源码注释中明确提到了泊松分布,这是理解其原理的关键。
在默认负载因子0.75下,哈希值分布均匀时,不同链表长度出现的概率如下表所示(基于泊松分布公式计算):
链表长度(k) 发生概率§
0 0.60653066
1 0.30326533
2 0.07581633
3 0.01263606
4 0.00157952
5 0.00015795
6 0.00001316
7 0.00000094
8 0.00000006
从数据中可以看出,链表长度达到8的概率已经是一个极小概率事件。因此,将阈值设置为8意味着:在绝大多数正常情况下,根本不会触发树化。而一旦触发,则说明当前桶的冲突情况已经偏离了理想状态,必须通过升级为红黑树来保证性能。
⚖️ 性能与空间的精细权衡
选择8作为阈值,也是在对链表和红黑树的性能曲线进行充分评估后做出的决策。
• 链表的优势与劣势:在元素数量较少时(比如长度小于6),链表的遍历速度很快,并且其节点结构简单,内存占用小。但如果链表长度持续增长,其查询性能会线性下降。
• 红黑树的优势与劣势:红黑树是一种自平衡的二叉查找树,它能保证在最坏情况下查询、插入、删除的时间复杂度都是O(log n)。当长度增长时,其性能衰减远慢于链表。但代价是节点需要维护父指针、子指针、颜色标识等,占用更多内存,且插入删除时可能需要复杂的旋转操作来保持平衡。
通过测试和计算发现,当链表长度超过8时,红黑树的查询效率开始显著超过链表,其带来的性能收益足以抵消其额外的空间开销和维护成本。因此,8是一个在时间和空间之间取得最优平衡的临界点。
🔄 重要的附加条件与缓冲设计
需要注意的是,链表长度达到8只是树化的必要条件之一,而非充分条件。
- 数组长度需达到64:当链表长度达到8,但HashMap内部的数组长度小于 MIN_TREEIFY_CAPACITY(默认64)时,HashMap会选择先进行扩容(resize),而不是立即将链表树化。因为扩容可以增加桶的数量,有可能将原来长链表中的节点重新分散到不同的新桶中,从而缩短链表长度。这是一种成本更低的解决冲突的方式。
- 树化与链化阈值不同:树化阈值是8,但当红黑树中的节点由于删除操作而减少到6时,它会退化成链表。将退化的阈值设置为6而不是8,目的是形成一个缓冲区间。这可以防止在元素数量在8附近波动时,频繁地进行树化和链化转换,从而保证性能稳定。
💎 总结
总而言之,HashMap将链表转为红黑树的阈值设为8,是一个经过严谨数学论证和大量性能测试得出的工程最优解。它体现了以下设计思想:
• 基于统计的乐观策略:相信在绝大多数情况下哈希冲突是轻微的,优先使用高效的链表。
• 为极端情况兜底:通过树化机制防止在哈希函数不理想或遭遇恶意碰撞攻击时,性能急剧下降。
• 平衡的艺术:在时间效率(查询性能)和空间效率(内存占用)之间,以及在常规情况性能和极端情况性能之间,取得了精妙的平衡。
希望这个解释能帮助你更深入地理解HashMap的设计哲学。