先省流,说结论:
HashMap去树化有两种情况
在树拆分过程中,拆完的两棵树分别判定,如果总节点<=6的话就去树化
在去除树节点时,通过一系列条件判定,一般会在树节点2-6时进行去树化
前言
之前在准备面试背八股时,看了一堆HashMap树化的东西,但是似乎没啥人讲去树化,而有的文章可能会略点一二,但是似乎解答也不统一(非引战,只做讨论)
叠甲
- 本人才疏学浅,对HashMap只略懂一二。如果内容有问题,请指正orz😂,我们会第一时间去研究和修改内容,非常感谢(^\_^)。
- 本文使用Java8源码,目前最新的Java22源码几乎没有对去树化进行修改,总流程一致。
- 本文着重讲去树化,一些顶层方法如resize、put、remove并不做展开,需要读者自己can can。
正文
1. 什么是去树化,如何去树化
- 去树化指的就是HashMap中当红黑树节点数过少时,将红黑树转为链表的操作
- HashMap去树化使用的是
untreeify
方法,流程很简单,就是从当前的node开始,逐个使用replacementNode
把树节点改为普通节点
final HashMap.Node<K,V> untreeify(HashMap<K,V> map) {
// hd 链表头 tl 链表尾
HashMap.Node<K,V> hd = null, tl = null;
// 从当前Node开始,逐个调用replacementNode将书树节点换成链表节点
for (HashMap.Node<K,V> q = this; q != null; q = q.next) {
HashMap.Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
2. 哪里会调用去树化
HashMap中有两个地方会调用去树化,这也是大部分说法都忽视的。具体的方法:removeNode
和split
,由于split方法是大家津津乐道的 <=6去树化的理论, 就先说说split方法吧
split
中的去树化
split,顾名思义,就是把树撕成两棵树,至于是怎么撕为什么撕,各位八股好手应该记得resize
方法中根据Node中根据hash值的高bit位对链表/树进行拆分,这是老生常谈的玩意了,这里就不做展开,感兴趣可以看这个:HashMap之resize详解。而这个过程中如果哪一棵树节点<=6,就进行untreeify/去树化
具体流程
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// ...通过hash值的高位bit,分出hi,lo两条链表
// 分别判断它们长度是否<=UNTREEIFY_THRESHOLD(6)
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
removeNode
中的去树化
这个方法大家其实很常用,就是remove
的底层调用,而里面作者的原话:The test triggers somewhere between 2 and 6 nodes, depending on tree structure,说人话就是大概2-6个节点去树化
<p align=center>这里使用的是IDEA的Translation插件,强推!非广!绝对的顶级!</p>
具体流程
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// ... 定位出节点所在树的根节点
// 调用untreeify条件
if (root == null|| (movable
&& (root.right == null|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
// ... 节点的删除/替换
}
代码中的条件root.right == null|| (rl = root.left) == null|| rl.left == null
比较抽象,我画个图吧,假如图中A\~C中有一个节点为null则条件成立(源代码这里注释了个tooSmall,后面我们就称这个条件为tooSmall情况吧)
那在什么情况下条件成立呢,根据红黑树的规则
ok,到这里可以算是破案了,HashMap去树化有俩方法,而且条件不一致,难怪网上说的总是奇奇怪怪。但是,真的吗,我不信(鲁豫脸),本着实践出真知的原则,我想再实操一下
实践环节
实践内容仅为印证观点,嫌我啰嗦的小伙伴可以直接跳过
为了代码量小点,导入了lombok,小伙伴copy了代码的话可能要适配一下
感兴趣的小伙伴也可以copy代码(为了不影响观感,我把代码统一放在后面)
下面我会分别复现split和tooSmall这两种情况
1. split中出现单链<=6情况
这个场景实在是很难复现,因为
- 我需要通过hash值相等才能让节点都落入同一个hash槽
- 但是在split时由于hash值一致,节点只会归到同一条链表而不是散列在两条链表中
- 如果不散列在两条列表,最后的长度也不会<=6
不过没事,我还想到了个无赖方法😁:反射
- 重写
hashCode
和equals
方法,让所有node都进入到同一个hash槽,生成红黑树 - 通过反射修改节点hash值
- 插入大量无关节点,触发HashMap扩容
- 扩容中,split方法根据高位bit拆分出hiHead和loHead两条链表,而由于我把hash值改了,原本的节点会随机进入链表,最后大概率会出现一条链表<=6的情况
2. removeNode中树出现tooSmall情况
- 重写
hashCode
和equals
方法,让所有node都进入到同一个hash槽,并生成红黑树 - 一边删除树的节点,一边对树的情况进行打印和判断
由于这种情况可能的节点比较多,我就只给出最少节点和最多节点的部分操作吧
2个节点
可能会有小伙伴问:画的节点不是三个吗,为什么算两个
因为是先判断条件再删除结点的,在删除节点0操作时仍然不会出现tooSmall
而是在删除后满足tooSmall
条件,而在删除2、4、8任意一个节点时进行判断,才会触发去树化
5个节点
由于情景很多,我只复现了去树化时节点为2和节点为5,感兴趣的小伙伴可以copy下代码,通过debug一个一个删除节点来尝试不同的树结构
TIPS🤔:很怪,我想不到作者提出的6个节点就去树化的情况,如果有小伙伴能够复现也可以告诉我一下嘿😀
总结
个人认为网上大部分关于HashMap untreeify
的观点是有待商榷的,个人感觉应该是:
HashMap去树化有两种情况
1.在树拆分过程中,拆完的两棵树分别判定,如果总节点<=6的话就去树化
2.在去除树节点时,通过一系列条件判定,一般会在树节点2-6时进行去树化
后语
第一次写文章,没想到这么累orz😭,要考虑的东西太多了图片比例深浅色主题适配文案风格啥都得想,单单写这一篇小文章就小一天过去了,实在太佩服那些写长文的大佬了orz💪。
btw有的小伙伴如果是背八股的时候看到这篇文章,可能会问这个考不考。
我觉得没人会考这个的,但是我觉得不一定要面试官问你才说这个知识点,可以试着把面试官"勾引"到这个问题上来,我曾经就得手过一次:先借机点一下这个问题,接着面试官问,那刚刚这个问题你有了解过答案吗。嘿嘿,我的回合😍!个人感觉这样不仅能拿到一点面试主动权,而且也是一个比较加分的点,能体现出你不只是八股好手,还有去深入研究过源码这样嘿嘿。
最后再叠一个甲,这是我的处女文,如果文章内容出现表达不清晰/逻辑不通/内容错误等地方,请谅解,如果你能指出问题,非常感谢orz。
如果感觉文章不错的话,希望能点一个赞,这是对我们的莫大支持,非常感谢嘿嘿❤️。
源代码
/**
* 重写了hashcode和equals方法
* 1.在put过程中因为hashcode相同会进入同一hash槽
* 2.因为equals比较的是value,节点于链表/树上的每个节点都不同,追加到链表/树上
*/
@Getter
@AllArgsConstructor
class CustomKey {
private Integer value;
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object obj) {
return obj instanceof CustomKey && ((CustomKey) obj).getValue() == this.value;
}
}
public class Test {
public static void main(String[] args) {
tooSmallWithFiveNode();
tooSmallWithTwoNode();
split();
}
private static Map<Object, Integer> buildMap() {
// 由于树化需要table大于64,预设为64,不然需要放入到11个节点时才树化
// 原因可参考 https://www.bilibili.com/video/BV1fh411y7R8?p=539
Map<Object, Integer> map = new HashMap<>(64);
// 让0-8所有节点都在条链上,在8节点进入时,通过判定到当前链表长度(binCount)大于8,触发树化
for (int i = 0; i < 9; i++) {
CustomKey customKey = new CustomKey(i);
map.put(customKey, i);
}
return map;
}
private static void removeNodeAndPrint(Map<Object, Integer> map, Integer i) {
System.out.println("去掉节点" + i);
map.remove(new CustomKey(i));
printTreeStructure(map);
}
@SneakyThrows
private static void printTreeStructure(Map<Object, Integer> map) {
java.lang.reflect.Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
for (Object node : table) {
if (node != null) {
Class<?> treeNodeClass = Class.forName("java.util.HashMap$TreeNode");
if (treeNodeClass.isInstance(node)) {
printTreeNode(treeNodeClass.cast(node), "", true);
} else {
System.out.println("已经不是树结构,此时节点数" + map.size());
}
}
}
}
@SneakyThrows
private static void printTreeNode(Object node, String prefix, boolean isTail) {
Class<?> nodeClass = Class.forName("java.util.HashMap$Node");
java.lang.reflect.Field keyField = nodeClass.getDeclaredField("key");
java.lang.reflect.Field valueField = nodeClass.getDeclaredField("value");
java.lang.reflect.Field leftField = node.getClass().getDeclaredField("left");
java.lang.reflect.Field rightField = node.getClass().getDeclaredField("right");
java.lang.reflect.Field redField = node.getClass().getDeclaredField("red");
keyField.setAccessible(true);
valueField.setAccessible(true);
leftField.setAccessible(true);
rightField.setAccessible(true);
redField.setAccessible(true);
Object key = keyField.get(node);
Object value = valueField.get(node);
Object left = leftField.get(node);
Object right = rightField.get(node);
boolean isRed = redField.getBoolean(node);
System.out.println(prefix + (isTail ? "└── " : "├── ") +
(isRed ? "红" : "黑") + "(" + key + ", " + value + ")");
if (left != null) {
printTreeNode(left, prefix +
(isTail ? " " : "│ ") + "L-", right == null);
}
if (right != null) {
printTreeNode(right, prefix +
(isTail ? " " : "│ ") + "R-", true);
}
}
public static void tooSmallWithFiveNode() {
Map<Object, Integer> map = buildMap();
removeNodeAndPrint(map, 7);
removeNodeAndPrint(map, 1);
// 当前树结构: 4
// / \
// 2 8
// / \ / \
// 0 3 5 6
removeNodeAndPrint(map, 0);
// 当前树结构: 4
// / \
// 2 8
// \ / \
// 3 5 6
// 已经出现tooSmall情况,但是这里和树化同理(可见buildMap方法)
// 必须要再调用一次removeNode才能触发条件判断,从而去树化
removeNodeAndPrint(map, 2);
// 最后去掉2节点之后,hash槽中节点数为5
}
public static void tooSmallWithTwoNode() {
Map<Object, Integer> map = buildMap();
removeNodeAndPrint(map, 7);
removeNodeAndPrint(map, 1);
removeNodeAndPrint(map, 6);
removeNodeAndPrint(map, 5);
removeNodeAndPrint(map, 3);
// 当前树结构: 4
// / \
// 2 8
// /
// 0
removeNodeAndPrint(map, 0);
// 当前树结构: 4
// / \
// 2 8
// 已经满足tooSmall情况,但是这里和树化同理(可见buildMap方法)
// 必须要再调用一次removeNode才能触发条件判断,从而去树化
removeNodeAndPrint(map, 2);
// 最后去掉2节点之后,hash槽中节点数为2
}
private static void split() {
Map<Object, Integer> map = buildMap();
changeHash(map);
for (int i = 2; i < 100; i++) {
map.put(i, i);
}
}
private static void modifyHash(Object node) throws Exception {
if (node == null) return;
Class<?> nodeClass = Class.forName("java.util.HashMap$Node");
// 修改hash值
Field hashField = nodeClass.getDeclaredField("hash");
hashField.setAccessible(true);
hashField.setInt(node, (int) (Math.random() * 100 + 1));
// 递归修改左右子树
Class<?> treeNodeClass = Class.forName("java.util.HashMap$TreeNode");
Field leftField = treeNodeClass.getDeclaredField("left");
leftField.setAccessible(true);
Field rightField = treeNodeClass.getDeclaredField("right");
rightField.setAccessible(true);
modifyHash(leftField.get(node));
modifyHash(rightField.get(node));
}
@SneakyThrows
private static void changeHash(Map<Object, Integer> map) {
java.lang.reflect.Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
// 这里我把红黑树放table[1]中,就不做校验直接改了
Object root = table[1];
Class<?> treeNodeClass = Class.forName("java.util.HashMap$Node");
java.lang.reflect.Field hashField = treeNodeClass.getDeclaredField("hash");
hashField.setAccessible(true);
// 通过left和right循环获得所有节点,并修改hash属性
modifyHash(treeNodeClass.cast(root));
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。