头图

进入掘金浏览,效果更加哦😊~

先省流,说结论:

HashMap去树化有两种情况

在树拆分过程中,拆完的两棵树分别判定,如果总节点<=6的话就去树化

在去除树节点时,通过一系列条件判定,一般会在树节点2-6时进行去树化

前言

之前在准备面试背八股时,看了一堆HashMap树化的东西,但是似乎没啥人讲去树化,而有的文章可能会略点一二,但是似乎解答也不统一(非引战,只做讨论)

叠甲

  1. 本人才疏学浅,对HashMap只略懂一二。如果内容有问题,请指正orz😂,我们会第一时间去研究和修改内容,非常感谢(^\_^)。
  2. 本文使用Java8源码,目前最新的Java22源码几乎没有对去树化进行修改,总流程一致。
  3. 本文着重讲去树化,一些顶层方法如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中有两个地方会调用去树化,这也是大部分说法都忽视的。具体的方法:removeNodesplit,由于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

不过没事,我还想到了个无赖方法😁:反射

  1. 重写hashCodeequals方法,让所有node都进入到同一个hash槽,生成红黑树
  2. 通过反射修改节点hash值
  3. 插入大量无关节点,触发HashMap扩容
  4. 扩容中,split方法根据高位bit拆分出hiHead和loHead两条链表,而由于我把hash值改了,原本的节点会随机进入链表,最后大概率会出现一条链表<=6的情况

2. removeNode中树出现tooSmall情况

  1. 重写hashCodeequals方法,让所有node都进入到同一个hash槽,并生成红黑树
  2. 一边删除树的节点,一边对树的情况进行打印和判断

由于这种情况可能的节点比较多,我就只给出最少节点和最多节点的部分操作吧

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));
    }
}

GEDY
1 声望0 粉丝

I write.