JAVA中为什么Map桶(链表)长度超过8才转为红黑树

探讨了HashMap中链表转换为红黑树的原因及其阈值设定为8的理由。通过概率统计和空间利用分析,解释了这一设计如何平衡了查找效率与存储成本。

为什么要转换?

因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

为什么阈值是8?

转换后存储的数据结构TreeNodes占用空间是普通Nodes的两倍,只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多是由TREEIFY_THRESHOLD的值决定的。

在hashCode离散性很好的情况下,树型bin(桶,即bucket,HashMap中hashCode值一样的元素保存的地方)用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。事实上,在随机hashCode的情况下,在bin中节点的分布频率遵循如下的泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution)。

在扩容阈值为0.75的情况下,(即使因为扩容而方差很大)遵循着参数平均为0.5的泊松分布。忽略方差,按公式
在这里插入图片描述
计算,概率如下:

长度概率
00.60653066
10.30326533
20.07581633
30.01263606
40.00157952
50.00015795
60.00001316
70.00000094
80.00000006

如上,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。

大部分情况下,链表存储能节约存储空间同时有着良好的查找性能;极个别情况下,节点数达到8个,转为红黑树,能获得更好的查找性能,同时因为是个别情况,不需要大量的存储空间。

所以,阈值8是时间和空间的权衡,是根据概率统计决定的。不得不感叹,发展30年的Java每一项改动和优化都是非常严谨和科学的。

附. JDK(1.8.0_45)中的相关注释

HashMap类第174~197行

     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 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
     * more: less than 1 in ten million

ConcurrentHashMap中第327~349行也有关于此的说法,大同小异。

     * The main disadvantage of per-bin locks is that other update
     * operations on other nodes in a bin list protected by the same
     * lock can stall, for example when user equals() or mapping
     * functions take a long time.  However, statistically, under
     * random hash codes, this is not a common problem.  Ideally, the
     * frequency of nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average, given the resizing threshold
     * of 0.75, although with a large variance because of resizing
     * granularity. Ignoring variance, the expected occurrences of
     * list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The
     * first values are:
     *
     * 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
     * more: less than 1 in ten million
### JavaHashMap链表转换为红黑树实现原理 在 Java 中,`HashMap` 使用数组加链表的数据结构来存储键值对。为了提高性能,在特定条件下,当链表长度超过一定阈值链表会被转换成红黑树。 #### 阈值设定 默认情况下,当某个中的节点数目大于等于 `TREEIFY_THRESHOLD`(通常设置为8),并且哈希表的容量不小于最小树化容量 `MIN_TREEIFY_CAPACITY`(通常是64)链表将会被转换为红黑树[^3]。 #### 转换过程 具体来说,当向 `HashMap` 插入新元素并检测到某条链上的结点数达到了上述提到的阈值之后,程序会调用 `treeifyBin()` 方法来进行实际的转换操作。此方法内部实现了从单项链表至平衡二叉查找树即红黑树之间的转变逻辑。 以下是简化版的代码片段用于展示这一机制: ```java public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { binCount = 1; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if (++i >= n) break; if (((e = tab[i]) == null) || (e.hash != hash || (!Objects.equals(e.getKey(), key)))) continue; oldVal = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st node treeifyBin(tab, hash); ... } } ... } } ``` 这段代码展示了当尝试在一个已存在的非空位置插入新的映射关系所执行的一系列动作。特别注意的是最后一部分关于计数值 `binCount` 达到了 `TREEIFY_THRESHOLD - 1` 后触发了 `treeifyBin` 函数调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值