jdk1.8 HashMap链表转红黑树从put到treeify

Posted zhangjin1120

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdk1.8 HashMap链表转红黑树从put到treeify相关的知识,希望对你有一定的参考价值。

什么情况下转红黑树?

下面两个条件必须同时满足,才会转红黑树

  • 当前插入链表的长度大于或等于8。
  • HashMap中的数组,长度大于或等于64。

如果只是链表长度大于等于8,数组长度没有达到64,只会扩容,不会转红黑树。

从put方法到红黑树插入的具体过程

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //这个tab马上会被赋值为成员变量里的table
        HashMap.Node<K,V>[] tab; 
        HashMap.Node<K,V> p; 
        int n, i;
        // 如果tab数组未被初始化,则初始化该数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // hash值对应的tab[i]为空,直接放到tab数组中,作为第一个元素
        if ((p = tab[i = (n - 1) & hash]) == null){
            tab[i] = newNode(hash, key, value, null);
        }else {
            //新的链表结点
            HashMap.Node<K,V> e; 
            K k;
            //如果hash值恰好与对应桶的首个对象p一样,那么不用考虑,直接替换
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果Node的类型是红黑树,将值更新到树中
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 依次遍历链表,binCount主要用来判断什么时候由链表转换成红黑树
                for (int binCount = 0; ; ++binCount) {
                    // 如果遍历到链表末端,还没有插到链表中
                    if ((e = p.next) == null) {
                        //生成一个新的节点,放到链表的末尾,newNode方法并没有做什么
                        //只是把hash,key,value整合到一个新的Node中。
                        p.next = newNode(hash, key, value, null);
                        // 链表长度大于等于8(注意binCount的开始值是0,如果是1,
                        //就是(binCount>=TREEIFY_THRESHOLD)
                        //treeifyBin()内部不一定转红黑树,还要看数组长度是否到了64
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash); 
                        break;
                    }
                    // e就是p.next,那么e.hash如果与要put的key碰撞到,那么直接退出循环,此处的逻辑判断与桶首个对象的判断逻辑一样
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //如果没找到相同的key和hash 那么将e赋值为当前p,让他到下次循环中
                    p = e;
                }
            }
            // e!=null 代表原来Map中存在这个key
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果onlyIfAbsent=false,或者old_Value=null,不产生新的对象,直接替换value,默认是直接替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //将这个元素放到链表尾部 1.7之前是头插
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 如果桶的数量超过阈值
        if (++size > threshold)
            //重新分布桶,如果多线程的时候  会出现循环链表的情况,造成CPU升高,值错乱
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    //上面用到的newNode,并没有做啥。
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

/**
 * tab:元素数组,
 * hash:hash值(要增加的键值对的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
 
    int n, index; Node<K,V> e;
    /*
     * 如果元素数组为空 或者 达不到成树的条件
     * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换。
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 扩容
 
    // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
    // 根据hash值和数组长度进行取模运算后,得到链表的首节点
    else if ((e = tab[index = (n - 1) & hash]) != null) { 
        TreeNode<K,V> hd = null, tl = null; //首(head)尾(tail)节点
        do { 
            TreeNode<K,V> p = replacementTreeNode(e, null); //根据Node新建一个TreeNode
            if (tl == null) //hd只是保存新生成的双向链表
                hd = p; // 这行代码在循环中只运行一次。
            else {  
                p.prev = tl; //新结点的前驱赋值为tl
                tl.next = p; // 尾节点的 后继指向 新节点
            }
            tl = p; // 尾结点向后移动
        } while ((e = e.next) != null); // 继续从头到尾遍历单链表
 
        // 到目前为止 也只是把Node对象转换成了TreeNode对象,
        //把Node单向链表转换成了TreeNode双向链表
 
        // 转换后的双向链表,替代原来位置上的单向链表
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//双链表转红黑树
    }
}


    TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
	
//接着看treeify做了什么

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null; // 定义树的根节点
    // 遍历链表
    // 调用treeify的代码是:hd.treeify(tab); 这个this,就是之前的hd
    for (TreeNode<K,V> x = this, next; x != null; x = next) { 
        next = (TreeNode<K,V>)x.next; // 给next赋值
        x.left = x.right = null; // 设置当前节点的左右节点为空
        if (root == null) { // 这个只运行一次
            x.parent = null; // 当前节点的父节点设为空
            x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
            root = x; // 根节点指向到当前节点
        }
        else { // 根节点赋值完成后
            K k = x.key; // 取得当前链表节点的key
            int h = x.hash; // 取得当前链表节点的hash值
            Class<?> kc = null; // 定义key所属的Class
            for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出
                // GOTO1
                int dir, ph; // dir 标识方向direction(左右)、ph标识当前树节点的hash值
                K pk = p.key; // 当前树节点的key
                if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
                    dir = -1; // 标识当前链表节点会放到当前树节点的左侧
                else if (ph < h)
                    dir = 1; // 右侧
 
                /*
                 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
                 * 如果还是相等,最后再通过tieBreakOrder比较一次
                 */
                else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
 
                TreeNode<K,V> xp = p; // 保存当前树节点
 
                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
                 * 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点  再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
                 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
                 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
                 */
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp; // 当前链表节点 作为 当前树节点的子节点
                    if (dir <= 0)
                        xp.left = x; // 作为左孩子
                    else
                        xp.right = x; // 作为右孩子
                    // 这里面包含了左旋操作
                    root = balanceInsertion(root, x); 
                    break;
                }
            }
        }
    }
 
    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
    moveRootToFront(tab, root); // 单独解析
}

总结一下:

put操作中,如果入参结点的key不存在,则通过尾插法,将入参结点插入到单链表的末尾。然后判断链表长度是否达到8,如果达到8,并且HashMap中数组长度已经达到64,则会将入参结点所在的链表,转为TreeNode双向链表,然后再将TreeNode双向链表转为红黑树。

先分析到这里,后续再分析,双链表到底是怎么转为红黑树的。

以上是关于jdk1.8 HashMap链表转红黑树从put到treeify的主要内容,如果未能解决你的问题,请参考以下文章

HashMap底层数据结构之链表转红黑树的具体时机

ConcurrentHashMap在jdk1.8中的改进-时间复杂度从O(n)到O(log(n))-链表转红黑树的值是8

HashMap什么时候会触发链表转红黑树

红黑树的理解与Java实现

JDK1.8 HashMap为什么在链表长度为8的时候转红黑树,为啥不能是9是10

JDKJDK1.8 HashMap两种扩容的情况和转红黑树