一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
HashMap在单向链表超过一定长度的时候,会转化为红黑树,而红黑树在节点数量足够少的情况下会转化为单向链表
单向链表转化为红黑树
单向链表转化为红黑树有两个条件:
- 在单向链表中添加新节点后,链表中的节点总数大于TREEIFY_THRESHOLD(默认8)
- table数组的长度大于MIN_TREEIFY_CAPACITY(默认64)
在阅读源代码中我们可以看到putVal()方法中调用了treeifyBin()的方法,但是即使是真正的调用了treeifyBin()方法也不一定真正的会进行树化,还需要满足第二个条件,如果说不满足第二个条件,将进行HashMap的扩容操作。
在进行树化的过程中,会对单向链表进行遍历,并为每一个节点创建对应的TreeNode节点,然后这些TreeNode节点会首先构成一个双向链表结构,这个双向链表在未来可能进行的将红黑树转化为单向链表发挥关键作用;
对双向链表结构进行红黑树的转化,根节点默认为双向链表的头节点,对双向链表进行遍历,将它们一次添加到新的红黑树中,并在添加完成后进行对红黑树的平衡处理。
红黑树转化为单向链表
当进行了一些操作后,红黑树变的足够小的时候,会将红黑树转化为单向链表;这种会将红黑树转化为单向链表的操作主要是有两种情况:
- HashMap进行扩容操作时
- 当使用HashMap集合中的remove方法进行节点移除时
HashMap扩容操作时,为了保证依据(Key键信息的hash值)&(数组长度-1)仍然能定位到正确的节点存储的数据索引位,需要依次对这些索引位上的红黑树进行拆分操作。如果拆分后的红黑树的节点总数小于或者等于UNTREEIFY_THRESHOLD常量值(默认6),那么可以将这棵红黑树转换为单向链表。
在使用remove方法进行节点移除,导致红黑树的根节点的左侧儿子节点或者右侧儿子节点为null,那么可以将这课红黑树转为单向链表;
对红黑树的遍历操作不是依据红黑树结构进行的,而是依据红黑树中隐藏的双向链表进行遍历的,将这个双向链表替换成符合要求的单向链表结构。
HashMap集合的扩容原理
HashMap的扩容操作主要分为以下两种场景:
- 当table数组为null或者长度为0时需要进行扩容操作;
- 在添加新的K-V节点后,当节点数量即将超过扩容门槛时需要进行扩容操作;
扩容的操作过程:
-
根据当前集合的大小,确定新的数组容量值和新的扩容门槛值
-
oldCap > 0
- 扩容前的数组容量大于HashMap设置的最大数组容量值,则设置下次扩容门槛值为最大数组容量值,并且不再进行扩容操作,返回原来数组大小
- 如果扩容前的数组容量大于DEFAULT_INITIAL_CAPACITY,设置新数组容量为原容量的2倍,并且设置扩容门槛值为原扩容门槛值的2倍;
-
oldThr > 0 隐含条件 oldCap = 0
- 将新的数据容量设置为原扩容门槛值
-
其他情况
- 新的数组容量为默认的16,扩容门槛为 16 * 0.75
-
如果新的扩容门槛为0;新的扩容门槛为 新的数组容量值 * 当前的扩容因子
-
-
对原数组中的K-V节点进行调整,使集合结构恢复平衡
- 只有在扩容前数组不为空的情况下,才需要对原数组中的各个K-V键值进行再平衡处理。
- 遍历当前数组索引位,如果当前数组索引位上没有任何节点,则不需要进行再平衡处理。
- 如果e.next为null的话,说明当前数组索引位上只有一个K-V键值对,那么需要使用“e.hash & (newCap-1)”进行索引位的重新计算;
- 如果当前索引位为树节点,则说明桶结构上为一颗红黑树,使用TreeNode.split()方法进行再平衡处理。主要原理依然是遍历树结构中的双向链表,判断 e.hash & oldCap == 0 则存放在原索引位的桶结构上,否则存放在原索引位 + 原数组长度的桶结构上;如果拆分后红黑树的节点数小于 6 ,那么将红黑树结构转化为链表结构;
- 如果当前桶结构上为单向链表,则遍历该链表,如果e.hase & oldCap == 0,则存放在原索引位的桶结构上,否则存放在原索引位 + 原数组长度的桶结构上;