美文网首页
HashMap为什么要先插入再扩容JDK1.8

HashMap为什么要先插入再扩容JDK1.8

作者: youxiaochen | 来源:发表于2022-03-14 17:13 被阅读0次

    JDK1.8开始HashMap为什么要先插入后扩容,网上查找有说先扩容再插入可以少遍历之类的,其实不管是先扩容还是先插入,它的原则还是尾插法都是避免不了要遍历的,那它为什么还是要先插入呢,只要看插入逻辑和扩充逻辑做了哪些操作就知道了,以下也 只是个人的理解,如有错误欢迎指点

    首先看下JDK1.8 HashMap插入的源码
    1:插入操作如果数组中的节点是红黑树是往节点中插入节点, 如果是链表的时候可能会要从链表升级成红黑树,似乎先插入再扩容还是先扩容后插入都是没影响的都是要遍历, 那问题原因就在扩容机制里
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
        for (int binCount = 0; ; ++binCount) {
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);
                /**
                *  这里要满足当前链表的长度>=7,遍历是从根节点开始,因此相当于包括根节点有8个时再插入就会调用此方法
                */
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    
    
    2:再看看扩容的源码中有个关键的代码((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    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);
        }
    }
    

    扩容的时候如果红黑树节点的个数<=6个时,就会降级成链表了

    HashMap为何将红黑树降级链表的阈值设置成6而不是7, 就是防止节点在红黑树与链表之间因插入扩容而频繁的转变,频繁的转变是要消耗性能,而插入时的操作中将红黑树变成链表的触发条件就是扩容
    下面就举例频繁转变临界点的情况 ,根据频繁转变临界点的情况对比来说明原因,假设插入的节点刚好都在当前的链表或者红黑树的索引位置上
    临界点情形一

    假设当前插入节点是红黑树结构有7个, 如果先扩容后会有1个节点移到别的index索引时,导致当前红黑树的节点变成6个就会降级成链表再插入变成7个节点的链表。
    而如果是先插入的话就是8个节点红黑树,然后扩容即使有1个节点移到别的位置也还是7个节点也不会转变成链表
    \color{red}{而且即使大于7个节点的红黑树先扩容都有可能降成链表}
    \color{red}{而先扩容再插入时如果连续再插入2个相同索引位置的节点链表就又会变红黑树}

    临界点情形二

    和情形一逻辑相反的情况,如果当前插入节点为8个且为链表的时候,如果先扩容后会有1个节点移到别的index索引时再插入还是8个节点链表不会变红黑树,而如果是先插入就会变成红黑树再扩容有1节点移动时变成8个节点的红黑树,如果此时移除节点又有可能降成链表,先分析下移除节点时降链表条件的源码

    //这是移除时的部分源码
    if (root == null || root.right == null ||  (rl = root.left) == null || rl.left == null) {
           tab[index] = first.untreeify(map);  // too small
           return;
    }
    

    可以看出在移除节点的时候不一定是6个节点就会导致红黑树降级成链表,根据红黑树的特点有可能当前链表 3 <= Nodes <=6时才会降级成链表
    \color{red}{而且只有链表为8个节点的时候先插入才会变红黑树}
    \color{red}{先插入再扩容时如果再连续移除最少2个最多5个节点的时候才又会降成链表}

    \color{red}{结论:通过以上两种临界情形对比,很明显如果先扩容再插入时频繁转变的概率要比先插入后扩容频繁转变的概率高}

    这里也给一个模拟HashMap造出红黑树的测试代码,红黑树为7个节点的时候,从64扩容到128时导致了key为64的节点移到第64的索引中去,而红黑树就变成了链表了
    public static void main(String[] args) {
            //默认64个大小hashMap,只要数组长度>=64, 且链表 》=8时时才会转红黑树
            HashMap<Integer, String> map = new HashMap<>(64);
            map.put(0, "王二麻子 0");
            map.put(64, "王二麻子 1");
            map.put(128, "王二麻子 2");
            map.put(256, "王二麻子 3");
            map.put(512, "王二麻子 4");
            map.put(1024, "王二麻子 5");
            map.put(2048, "王二麻子 6");
            map.put(4096, "王二麻子 7");
            map.put(8192, "王二麻子 8");
            //以上步骤就会生成在第0的索引上的一个9个节点的红黑树结构
            map.remove(8192);
            map.remove(4096);
            //移除二个后此时是一个有7个节点的红黑树
            //这一步添加41个其他位置上的节点,只要不在0节点上就行,让节点总数达到 64*0.75 = 48个
            for (int i = 1; i <= 41; i++) {
                map.put(i, "张三 " + i);
            }
            //插入第49个时触发扩容,此后红黑树结构就变成链表结构了
            map.put(42, "李四");
        }
    
    以上只是个人观点,如有错误欢迎指点

    相关文章

      网友评论

          本文标题:HashMap为什么要先插入再扩容JDK1.8

          本文链接:https://www.haomeiwen.com/subject/avgjdrtx.html