源码速读:HashMap中的树会不会转成链表?
2023-06-13 09:15:36 时间
问1:报告!我用的JDK1.6中没有树,都是链表
问2:应该会吧?
小伙子,你很有天赋啊!
-- 那了解。那bye bye...
同学留步。你知道什么时候会做这个转换吗? --这个,这个。。。应该是删除时吧 优秀!除了删除时,还有没有别的场景?
算了,不卖关子了。 直接上结论:
对HashMap进行remove或resize时,如果冲突元素小于6,则树结构将转为链表。
下面是高能代码,没有做好心理建设的,赶快回避!!!
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
* 返回一个非树节点链表,该链表替换从该节点链接的树节点。
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
看下这个方法有哪几个地方调用呢?
可以看到有3个地方会进行树结构转换成链表的操作,来一个一个看下:
java.util.HashMap.TreeNode#removeTreeNode
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
* 移除在此调用之前必须存在的给定节点。
* 这比典型的红黑树删除代码更混乱,
* 因为我们不能将内部节点的内容与由“next”指针固定的叶继承节点交换,
* 这些“next”指针在遍历期间可以独立访问。所以我们交换树的引用。
* 如果当前树的节点似乎太少,则将bin转换回普通bin。(根据树的结构,测试触发2到6个节点)。
*/
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
//first 为当前桶的头结点,root为树的头结点 rl为根节点的左子树节点
TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;
// succ 为当前节点的下一节点 pred为当前节点的上一个节点(用来关联双链表的上下节点)
TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;
// 如果当前节点无前一节点,则当前节点为头结点,则直接将当前节点的下一节点设置为头结点,
// 否则的话将当上一节点(pred)的下一节点指针指向下一节点(next)
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
// 如果下一节点不为空 说明当前节点不是尾节点,
// 则将下一节点(next)的前一节点指针指向前一节点(pred),这样,当前节点在双链表中就脱离出来了
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
// 这个是判断当前红黑树的结构是否太小,
// 当然这种情况下是不存在,如果太小的话,
// 当前桶结构就是单链表了,而不是红黑树了.
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// 上面解析并使当前链表节点脱离双链表,下面就是使当前树节点脱离红黑树
// p为当前树节点,
// pl为当前树节点的左子树节点,
// pr为右子树节点,
// replacement为要替代当前树节点的节点(下面简称为当前树节点,左子树节点,右子树节点和替换节点)
TreeNode<K, V> p = this, pl = left, pr = right, replacement;
// 如果左右子树节点都不为空 进入判断
if (pl != null && pr != null) {
TreeNode<K, V> s = pr, sl;
// 通过while循环遍历有右子树节点的最左侧的左子树节点,并赋值给s(下面简称为右子树最小树节点)
while ((sl = s.left) != null) // find successor
s = sl;
// 将当前节点的与s节点的颜色进行互换
boolean c = s.red;
s.red = p.red;
p.red = c; // swap colors
TreeNode<K, V> sr = s.right;
TreeNode<K, V> pp = p.parent;
// 下面这个判断是当前节点的右子树节点是否无子树节点,
// 如果没有则将当前节点的父子树节点指针指向右子树节点,右子树节点的右子树指针指向当前节点,
// 从而完成子树节点的替换
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
} else {
// 如果右子树节点有子树节点,
// 则将右子树最小节点与当前节点进行替换,
// 分别将当前节点的父子树节点的子树节点指针指向右子树最小节点,
// 原先右子树最小节点的父子树节点的子树节点指针指向当前节点,
// 右子树最小节点的右子树节点指针指向右子树,(到这里是完成了当前节点的右子树的替换,下面就是左子树替换)
TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
// 将当前节点左子树节点指针指向空
p.left = null;
// 将右子树最小树节点的右子树节点指针指向当前节点的右子树节点,
// 右子树最小树节点的右子树节点的父子树节点指向当前节点
if ((p.right = sr) != null)
sr.parent = p;
// 将右子树最小节点的左子树指针指向左子树节点,并将左子树节点的父子树节点指针指向右子树最小节点
if ((s.left = pl) != null)
pl.parent = s;
// 如果此时的右子树最小子树节点的父子树节点为空,则其为根节点,将其赋值给root,
// 否则的话,将父子树节点的的左/右子树节点的指针指向s
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
// 此时的sr是当前节点p的右子树节点,如果不为空则将replacement指向sr,否则指向p
if (sr != null)
replacement = sr;
else
replacement = p;
} else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
// 上面的三个else 如果当前节点有子树节点则将relpacement指向此子树节点,否则指向p
// 下面就开始使p脱离树了
//下面的判断 如果p有子树节点(无论是替换前还是替换后)即replacement != p
if (replacement != p) {
TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
// 如果此时的p为红色,则返回root,
// 否则进入balanceDeletion,balanceDeletion是重新调整树结构并返回调整后的树的根节点.这里就不在多说了
TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);
// 如果p无子树节点(无论是替换前还是替换后),
// 使此时的p的父子树节点的左/右子树节点指向null,
// 此时的p也算是脱离出来了
if (replacement == p) { // detach
TreeNode<K, V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
// 最后通过moveRootToFront重新定义双链表的头部节点使之与树的根节点相同
if (movable)
moveRootToFront(tab, r);
}
另外两个将树转换成链表,都在一个Function中。 笔记本屏幕小,截不全,就是这个方法:java.util.HashMap.TreeNode#split
源码的内容太多,直接来个图吧
如果想更一步了解,可以去原文查看哟。
小贴士:
- HashMap在JDK1.8及以后的版本中引入了红黑树结构,
- 若桶中链表元素个数大于等于8时,链表转换成树结构;
- 若桶中链表元素个数小于等于6时,树结构还原成链表。 一些考量: 因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。 链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。 假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
- Java 8 HashMap使用Node类替代了Entry类,它们的结构大体相同。一个显著地差别是,Node类具有导出类TreeNode,通过这种继承关系,一个链表很容易被转换成树。