美文网首页
HashMap源码之二:put方法

HashMap源码之二:put方法

作者: 涂豪_OP | 来源:发表于2018-08-02 17:40 被阅读22次

        上一篇讲了HashMap的概述,这次研究下HashMap的重要方法put;不管什么容器类,添加元素的方法总是重要的,不能忽略的:

        /**
         * Implements Map.put and related methods
         *
         * @param hash hash for key key的hash值
         * @param key the key   键值对中的键
         * @param value the value to put    键值对中的值
         * @param onlyIfAbsent if true, don't change existing value 当存入一个元素是,如果此元素
         *                     已经存在,onlyIfAbsent指的是是否替换已经存在的元素,如果他为true,就不替
         *                     换,否则替换;默认为true,替换之
         * @param evict if false, the table is in creation mode.    evict是指当前的hashmap是不是
         *              处于创建状态,默认是true,代表hashmap的table创建好了,可以put元素了
         * @return previous value, or null if none
         */
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            //tab是HashMap的数组;要插入一个元素,首先要计算出这个元素应该放入数组的哪个位置;位置确定
            //后,那个位置,可能已经存在一个元素,这时候就需要用待插入元素去更新原来的元素,或者在那个元素
            //后面插入;p就是那个原来的元素,当然,他可能为空;n是数组的长度;i是待插入的元素在数组上的索引
            Node<K,V>[] tab; Node<K,V> p; int n, i;
    
            //如果当前的数组没有创建,,或者数组里面没有元素,那么调用resize()方法
            //resize()方法的作用是初始化HashMap的数组或者对HashMap进行扩容。
            //resize()是一个非常重要的方法,一会详细分析
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
    
            //元素插入桶的位置是由数组长度和key的hash值共同决定的,把这两个值进行与
            //操作就能得出桶的索引;如果那个数组索引上没有元素,那么创建一个新的节点
            //并放入数组中,这样就完成了插入;newNode()非常简单,不单独分析。
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
    
            //如果i那个位置上已经存在元素,说明发生了hash碰撞,需要更新i这
            //个位置连接的红黑树/链表上的节点;或者在红黑树/链表上新增新的元素
            else {
                Node<K,V> e; K k;
    
                //因为两个不同的key可能会hash出同一个hash值,这个时候就产生hash碰撞了;发生碰撞的两个元素
                //,他们的key可能一样,也可以不一样;如果一样的话,直接更新好了;如果不一样,那么只能把待插入
                //的元素插入的p的后面,(k = p.key) == key || (key != null && key.equals(k))就是
                //判断两个元素的key是不是相等。
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
    
                    //key相等的话就把p指向e,后面用得上
                    e = p;
    
                //如果key不等,说明待插入的元素要插入到p的后面,这个p可能是红黑树中的一个节点,也有可能是链表
                //中的一个节点,p后面连接不同的数据结构,插入的方法是不一样的,这里首先判断p后面是不是红黑树
                else if (p instanceof TreeNode)
    
                    //以红黑树的方式插入待插入元素
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    
                //这个分支代表p后面连接这个一个链表,以链表的方式插入目标元素
                else {
    
                    //链表的插入就非常简单了,就是遍历该链表,然后一个个比较,必要的时候树化
                    //当然了,链表中的节点的key可能和待插入元素的key一样,如果一样的话,直
                    //接更新链表上的这个节点即可;如果没有一样的,那么在链表尾部插入该元素
                    //插入完成后,e就指向了空
                    for (int binCount = 0; ; ++binCount) {
    
                        //遍历到链表的尾部还没有发现有元素和目标元素的key一样,那么根据传进来的key和value创
                        //建一个全新节点插入链表尾部;注意,if里面已经对e赋值了,所以第二个if可以直接对e操作
                        if ((e = p.next) == null) {
    
                            //创建全新的节点并插入节点的尾部
                            p.next = newNode(hash, key, value, null);
    
                            //如果链表节点数量达到了阈值(8) - 1,那么调用treeifyBin进行链表的树化
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
    
                        //这里就是判断目标元素和链表中的节点的key值是否一样,如果一样就直接更新,简单
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
    
                //如果e不为空,说明进入了第一个else分支,第一个else分支是指数组中已经存在一个key的hash和
                //目标元素的key的hash相等的节点,如果e不为空,说明整个put其实就是更新老的节点
                if (e != null) { // existing mapping for key
    
                    //获取老的值
                    V oldValue = e.value;
    
                    //onlyIfAbsent是传进来的,默认是false;所以进入if
                    if (!onlyIfAbsent || oldValue == null)
    
                        //简单的赋值
                        e.value = value;
    
                    //afterNodeAccess在HashMap中是一个空实现,
                    //在LinkedHashMap里面倒是有实现,暂时不管
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
    
            //修改次数+1,HashMap是线程不安全的,这玩意就是为
            //了解决迭代的时候修改HashMap造成的异常,后面分析
            ++modCount;
    
            //如果HashMap的元素个数超过阈值,就调用resize()进行扩容
            if (++size > threshold)
                resize();
            //同afterNodeAccess,不管
            afterNodeInsertion(evict);
            return null;
        }
    

        纵观put方法,最重要,也是最难理解的是扩容方法resize()和树化方法treeifyBin(//参数略),在分析这两个方法之前,先研究下红黑树节点是怎么被插入红黑树中的:

            /**
             * Tree version of putVal.
             * 参数解释:
             *  1.map : HashMap,简单
             *  2.tab : HashMap中的数组,简单
             *  3.h : key的hash值
             *  4.k : key
             *  5.v : value
             */
            final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                           int h, K k, V v) {
                //key的Class对象
                Class<?> kc = null;
    
                //searched是一个一次性开关,仅对根节点生效,下面会解释
                boolean searched = false;
    
                //putTreeVal()是TreeNode的方法,这个类有个成员变量parent,代表这个节
                //点的父节点如果父节点不为空的话,那么先找到红黑树的根节点,root()方法就是
                //往死里遍历红黑树,最终找到根节点;如果父节点为空,说明他本身就是根节点。
                TreeNode<K,V> root = (parent != null) ? root() : this;
    
                //从根节点开始遍历红黑树
                for (TreeNode<K,V> p = root;;) {
                    int dir, ph; K pk;
    
                    //如果遍历到的节点的hash值大于目标key的hash
                    //值,其实节点的hash值就是key的hash值
                    if ((ph = p.hash) > h)
    
                        //如果遍历到的节点的key的hash目标元素的hash,那么说明
                        //这个元素应该插入到遍历到的节点的左边,此时将dir置成-1
                        dir = -1;
    
                    //反之置成1
                    else if (ph < h)
                        dir = 1;
    
                    //如果两个hash相等,key也相等,那么返回这个节点;看起来有点奇怪,怎么就
                    //直接返回了呢?新的value没有更新啊,其实在putVal的最后面已经进行了更新
                    else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                        return p;
    
                    //流程走到这里,说明待插入的元素的key的hash和当前节点的hash相等但是两个key本身不相等(此时就
                    //是发生了hash碰撞)。comparableClassFor的作用就是返回key的Class信息;如果key的类型实现了
                    //Comparable接口,那么返回key的类型信息,没有实现的话就返回null;因为红黑树的左右子节点之间的
                    //值是有大小之分的,要想有大小之分,就必须具有可比性,实现Comparable接口会让两个相同类型的变量
                    //之间可比;如果返回null,说明key没有可比性;dir = compareComparables(kc, k, pk)) == 0
                    //的意思是k和pk之间不可比,也就说待插入元素的key和节点的key之间不具有可比性
                    else if ((kc == null &&
                              (kc = comparableClassFor(k)) == null) ||
                             (dir = compareComparables(kc, k, pk)) == 0) {
    
                        //在进for循环之前,searched为false;当遍历到根节点时,进入if,if里面通过find
                        //方法搜索了整棵树,如果能找到目标节点当然更好,找不到就再调用tieBreakOrder找一次
                        if (!searched) {
    
                            TreeNode<K,V> q, ch;
    
                            //把searched置为true,在遍历根节点的所有子节点时,都不会进这个分支
                            searched = true;
    
                            //find方法就是在红黑树里面查找key和目标元素key相等的节点
                            //这里分左右子树分别递归查找,短路操作是为了在左子树中找到
                            //后无需在右子树中再次查找;如果找到了,就把这个节点直接返回
                            if (((ch = p.left) != null &&
                                 (q = ch.find(h, k, kc)) != null) ||
                                ((ch = p.right) != null &&
                                 (q = ch.find(h, k, kc)) != null))
                                return q;
                        }
    
                        //流程走到这里,说明还没有找到目标节点,也就是说目标元素的key和红黑树的节点的key的hash相等
                        //但是两个key本身之间无法比较;那就只能使出绝招了:tieBreakOrder;这个方法是调用系统的方
                        //法identityHashCode来比较,比较的也是hash,但是不是上面的hash了,感兴趣的可以了解下
                        dir = tieBreakOrder(k, pk);
                    }
    
                    //流程走到这,说明key的比较结果出来了,这就要插入了;只有当
                    //前节点没有子节点的时候才能插入,否则就要进行下一轮循环比较了
    
                    //把遍历到的节点赋值给xp,这里是把xp当做一个父
                    //节点来使用的,因为后面的流程会把p指向他的子节点
                    TreeNode<K,V> xp = p;
    
                    //如果dir小于0,说明目标元素要放在p的左子树,所以把p指向他的左子节点
                    //如果dir大于0,说明目标元素要放在p的右子树,所以把p指向他的右子节点
                    //如果p不为空,说p已经有左/右子节点了,拿到这个节点,开始下一轮循环
                    //如果p为空,说明已经循环到叶子节点了,这时候就要把目标元素插在叶子节点下面
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
    
                        //next是Node的属性,TreeNode是Node的子类,继承了这个属性;
                        //这个属性记录了树化之前链表上节点的顺序,估计是为了反树化准备的
                        Node<K,V> xpn = xp.next;
    
                        //创建一个叶子节点,newTreeNode方法比较简单,不多说,注
                        //意最后一个参数;也就是说,每个红黑树节点都有一个next属性
                        TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
    
                        //决定是插在左子树还是右子树
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
    
                        //将刚才创建的节点x赋值给他父节点的next属性
                        xp.next = x;
    
                        //将父节点赋值给刚才创建的子节点的parent和prev
                        x.parent = x.prev = xp;
    
                        //xpn是xp的next,xp又是遍历到的节点,所xpn是当前遍历到的节点的next果当前
                        //遍如历到的节点的next不为空,那么把当前遍历到的节点的prev指向刚才创建的节点
                        if (xpn != null)
                            ((TreeNode<K,V>)xpn).prev = x;
    
                        //修复红黑树的平衡性,比较复杂,暂时不管
                        moveRootToFront(tab, balanceInsertion(root, x));
                        return null;
                    }
                }
            }
    
    

        可以看到,putTreeVal方法还是有点复杂的;下面研究扩容方法:

        /**
         * Initializes or doubles table size.  If null, allocates in
         * accord with initial capacity target held in field threshold.
         * Otherwise, because we are using power-of-two expansion, the
         * elements from each bin must either stay at same index, or move
         * with a power of two offset in the new table.
         *
         * @return the table
         */
        //扩容的方法,还肩负着初始化HashMap的重任
        final Node<K,V>[] resize() {
            //扩容前老的数组,当然如果是初始化HashMap的话,这个数组就是空了
            Node<K,V>[] oldTab = table;
    
            //老数组的长度
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
    
            //老的扩容阈值
            int oldThr = threshold;
    
            //新数组的长度和扩容阈值
            int newCap, newThr = 0;
    
            //如果oldCap大于0,说明调用resize()是为了扩容
            if (oldCap > 0) {
    
                //若干老数组的长度超过了最大值,那么扩容失败
                //;直接返回老数组,并且把阈值设置成最大值
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
    
                //如果没有超过最大值,那么新数组的长度是老数组长度的两个倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    //阈值也翻倍
                    newThr = oldThr << 1; // double threshold
            }
    
            //如果oldCap == 0 ,说明是创建HashMap,oldThr
            //大于0的话,说明调用带一个参数的HashMap的构造函数
            else if (oldThr > 0) // initial capacity was placed in threshold
    
                //对于带一个参数的构造函数,HashMap的容量就是扩容阈值
                newCap = oldThr;
    
            //种子情况就是最常见的了;HashMap map = new HashMap(),啥都没指定
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
    
            //对于一个参数的构造函数,只确定了容量,并没有确定新的扩容阈值
            if (newThr == 0) {
                //计算新的扩容阈值,简单
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
    
            //不管是扩容还是调用某个构造函数创建HashMap,走到这,新的扩容阈值算是定下来了,赋值
            threshold = newThr;
    
            //创建数组,这个数组对HashMap的重要性无需多言
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    
            //把新数组指向刚才创建的数组
            table = newTab;
    
            //如果老数组不为空,那么扩容吧
            if (oldTab != null) {
                //遍历老数组,把数组元素拷到新数组里面
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
    
                    //取出节点赋值给e
                    if ((e = oldTab[j]) != null) {
    
                        //把老数组元素置空
                        oldTab[j] = null;
    
                        //如果e没有后继节点,那么将此节点存入新数组,说明这个桶只有一个元素,没有链表和红黑树
                        //,e.hash & (newCap - 1)就是计算数组的索引;注意哦,这里是移动节点,所以是拿节
                        //点的hash与数组长度 - 1进行与运算确定这个节点位于数组中的索引;而上面putVal方法操
                        //作的是元素,所以是拿元素的key的hash与数组长度 - 1进行与运算确定元素在数组中的索引
                        //ps:其实这两个hash是一样的
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        //如果桶里面装的元素是红黑树节点
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //走到这个分支,说明这个桶后面接的是链表
                        else { // preserve order
    
                            //引入了两个链表:low、high;loHead是低位链表low的头结点,loTail是low的尾节点
                            //hiHead是高位链表high的头结点,hiTail是尾节点;这两个链表是为了拆分老数组某个桶
                            //后面连接的链表用的
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
    
                            //循环获取链表中的节点
                            do {
                                next = e.next;
                                //这个操作猛如虎:oldCap是2的n次幂,形如100000......;e.hash & oldCap就是
                                //取e.hash的高位,相与的结果要么是e.hash的高位,要么是0;如果是0的话,进入if
                                if ((e.hash & oldCap) == 0) {
    
                                    //如果链表尾节点都为空,说明这个链表是空的,此时将e置为头结点
                                    if (loTail == null)
                                        loHead = e;
    
                                    //否则的话,就把e放到链表尾部
                                    else
                                        loTail.next = e;
    
                                    //同时把链表尾节点指向e
                                    loTail = e;
                                }
                                //如果相与结果不为0,那么就是e.hash的高位,进入else
                                else {
                                    //道理同上
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
    
                            //如果低位链表的尾节点不为空,那么将尾节点的后继节点置空
                            if (loTail != null) {
                                loTail.next = null;
    
                                //同时将低位节点放入第j个桶
                                newTab[j] = loHead;
                            }
    
                            //同上,不过是把高位节点放入低j + oldCap个桶
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            //返回新数组
            return newTab;
        }
    

        方法本身不算难,他的扩容思想如下:
        1.如果桶后面没接红黑树/链表,那么拿到这个节点的hash,与新数组的长度 - 1做与运算,确定桶的位置,然后把这个节点放入那个桶;
        2.如果桶后面接着一个链表,那么将此链表一拆为二;拆分的依据是用节点的hash与老数组做与运算,与运算只有两个结果,要么是0,要么是hash的高位;如果结果是0,那么此节点插入低位链表;如果结果不等于0,那么将此插入高位链表;最后把低位链表放入第j个桶,把高位链表放入第j + oldCap个桶;
        3.如果桶后面接着的是一棵树,那么调用split方法将红黑树拆分,下面研究下这个方法:

            /**
             * Splits nodes in a tree bin into lower and upper tree bins,
             * or untreeifies if now too small. Called only from resize;
             * see above discussion about split bits and indices.
             *
             *  拆分的思想是先将红黑树转化成两个链表;这两个链表必要的时候进行树化和反树化
             *
             * @param map the map   HashMap,没问题
             * @param tab the table for recording bin heads     新数组
             * @param index the index of the table being split  桶的位置
             * @param bit the bit of hash to split on   老数组的长度
             */
            final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    
                //红黑树的根节点
                TreeNode<K,V> b = this;
    
                // Relink into lo and hi lists, preserving order
                //引入四个红黑树节点,类似于链表的拆分,高低位的头尾节点
                TreeNode<K,V> loHead = null, loTail = null;
                TreeNode<K,V> hiHead = null, hiTail = null;
    
                //高低位节点的计数器
                int lc = 0, hc = 0;
    
                //通过节点的next往死里遍历红黑树,但是for循环可以这样用的吗
                for (TreeNode<K,V> e = b, next; e != null; e = next) {
    
                    //重新赋值next节点
                    next = (TreeNode<K,V>)e.next;
    
                    //把e的next置空,因为e要插到两个链表中的一个区,他的后继节点还不确定,所以置空
                    e.next = null;
    
                    //与操作,根链表的拆分是一毛一样的
                    if ((e.hash & bit) == 0) {
    
                        //如果低位尾节点是空,说明链表是空的,那么e就是头结点了
                        if ((e.prev = loTail) == null)
                            loHead = e;
    
                        //否则的话就插在尾节点后面
                        else
                            loTail.next = e;
    
                        //同时把尾节点指向e
                        loTail = e;
    
                        //低位节点数量+1
                        ++lc;
                    }
                    //同上
                    else {
                        if ((e.prev = hiTail) == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                        ++hc;
                    }
                }
    
                //经过上面的遍历,红黑树成功被拆成高低位两个链表,但是这另个链表的长度不一定
                //符合树化和反树化的要求,所以这里就进行判断;如果小于反树化的阈值,那么把链
                //表中的红黑树节点TreeNode转化成普通节点Node;否则的话就把链表树化成红黑树
                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);
                    }
                }
            }
    

        拆分红黑树其实和拆分链表的道理一样,还算是比较简单的,下面介绍最后一个重要方法,树化:treeifyBin(Node<K,V>[] tab, int hash):

    /**
         * Replaces all linked nodes in bin at index for given hash unless
         * table is too small, in which case resizes instead.
         * 树化的第一步是将普通节点Node转化成红黑树节点
         */
        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
            //如果HashMap的容量没有达到MIN_TREEIFY_CAPACITY,那么优先选择扩容
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
    
            //下面就为树化做准备
            else if ((e = tab[index = (n - 1) & hash]) != null) {
    
                //申明头尾两个红黑树节点
                TreeNode<K,V> hd = null, tl = null;
    
                //往死里遍历链表
                do {
                    //将普通节点Node转换成红黑树节点TreeNode,这个转换非常简单,
                    //就是根据Node创建一个TreeNode对象,注意最后一个参数next为空
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    //如果为节点是空,那么说明链表是空的,刚刚创建的红黑树节点p就是头结点
                    //注意,这里说的链表是空的,不是指待转换的链表,而是创建了一个新的链表
                    if (tl == null)
    
                        //将头节点指向p
                        hd = p;
                    else {
    
                        //如果尾节点不为空,那么将新建的红黑树节点插入链表尾部
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null);
    
                //经过上面那一通折腾,原来的链表都变成一个全新的,连接红黑树节点的链
                //表;这里就将新的链表放入桶中;然后调用头结点的treeify进行真正的树化
                if ((tab[index] = hd) != null)
                    hd.treeify(tab);
            }
        }
    

        treeifyBin方法只是树化的一个热身步骤,最重要的是treeify方法:

            /**
             * Forms tree of the nodes linked from this node.
             * 这里才是真正的树化过程
             * @return root of tree
             */
            final void treeify(Node<K,V>[] tab) {
    
                //红黑树的根节点
                TreeNode<K,V> root = null;
    
                //遍历此链表,起点是调用treeify方法的节点,也就是链表的头结点
                for (TreeNode<K,V> x = this, next; x != null; x = next) {
                    //给next赋值,代表的是当前遍历到的节点的后继节点
                    next = (TreeNode<K,V>)x.next;
                    //首先将当前的节点的左右子节点置空,只有当前节点的next节点才能确定左右子节点
                    x.left = x.right = null;
    
                    //如果根节点是空的话,首先要确定根节点
                    if (root == null) {
    
                        //根节点是没有父节点的,所以将parent置空
                        x.parent = null;
    
                        //根节点必须是黑色的
                        x.red = false;
    
                        //将当前节点赋值给根节点
                        root = x;
                    }
    
                    //下面就是在根节点的下面插入子节点
                    else {
                        //首先拿到链表节点的key
                        K k = x.key;
    
                        //拿到链表节点的hash值
                        int h = x.hash;
    
                        //key的类型信息
                        Class<?> kc = null;
    
                        //以根节点为起点遍历红黑树,确定链表节点应该插到红黑树的哪个地方
                        for (TreeNode<K,V> p = root;;) {
    
                            //dir是链表节点和红黑树中的节点的比较结果;ph是红黑树节点的hash值
                            int dir, ph;
    
                            //拿到红黑树节点的key值
                            K pk = p.key;
    
                            //如果红黑树节点的hash值比链表节点大,说明链表节点应该插在红黑树节点左边,dir == -1
                            if ((ph = p.hash) > h)
                                dir = -1;
    
                            //如果红黑树节点的hash值比链表节点大,说明链表节点应该插在红黑树节点左边,dir == -1
                            else if (ph < h)
                                dir = 1;
    
                            //如果链表节点和红黑树节点的hash值一样,但是两者有没有可比性,那么只能出
                            //绝招了,用tieBreakOrder比较两个对象的内存地址的hash;这个过程之前说过
                            else if ((kc == null &&
                                      (kc = comparableClassFor(k)) == null) ||
                                     (dir = compareComparables(kc, k, pk)) == 0)
                                dir = tieBreakOrder(k, pk);
    
                            //将红黑树节点赋值给xp,xp就会别当做父节点来使唤
                            TreeNode<K,V> xp = p;
    
                            //根据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;
                            }
                        }
                    }
                }
                //确保链表头节点是红黑树的根节点
                moveRootToFront(tab, root);
            }
    

        可以看到,treeify本身不难,难的是修复红黑树平衡的方法balanceInsertion,这个方法没研究过,不敢妄言。
        自此,HashMap的put方法研究的差不多了。

    相关文章

      网友评论

          本文标题:HashMap源码之二:put方法

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