美文网首页
HashMap源码之resize方法

HashMap源码之resize方法

作者: 高手坟墓_ | 来源:发表于2019-07-06 17:24 被阅读0次

    1.扩容原理

    JDK7的时候先通过resize()方法对entry数组扩容,然后通过transfer()方法重新计算每个元素在新数组中的位置,对于JDK7的具体实现不再赘述。下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。 扩容后重新确定元素在数组中的索引.png 开始n为16,所以n-1的二进制表示1111,所以对于key1(0 0101)和key2(1 0101)来说,key & (n-1)计算出的结果是一样的,即(0101),当扩容后,n等于32,n-1的二进制表示(1 1111),此时key1(0 0101)和key2(1 0101)通过key & (n-1)计算出的结果就会不一样了,但会发现只是key2的计算结果比之前多了(1 0000),即为16,正好是扩容前数组的长度,由此发现数组扩容后,之前数组的元素在新数组中的索引,要么是原位置,要么时原索引加上扩容前的数组长度,JDK8的resize()方法即利用了这一点,不用一个一个元素的计算其在新数组中的位置,而只用将老的数组中的元素分组,一组是原索引,一组是原索引加上老的数组长度,从而提升了效率。 JDK8扩容过程示意图.png

    2.源码分析

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        
        // 原table中已经有值
        if (oldCap > 0) {
            // 已经超过最大限制, 不再扩容, 直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }        
            // 注意, 这里扩容是变成原来的两倍
            // 但是有一个条件: `oldCap >= DEFAULT_INITIAL_CAPACITY`
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }    
        // 在构造函数一节中我们知道
        // 如果没有指定initialCapacity, 则不会给threshold赋值, 该值被初始化为0
        // 如果指定了initialCapacity, 该值被初始化成大于initialCapacity的最小的2的次幂    
        // 这里是指, 如果构造时指定了initialCapacity, 则用threshold作为table的实际大小
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;    
        // 如果构造时没有指定initialCapacity, 则用默认值
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }    
        // 计算指定了initialCapacity情况下的新的 threshold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        
        //从以上操作我们知道, 初始化HashMap时, 
        //如果构造函数没有指定initialCapacity, 则table大小为16
        //如果构造函数指定了initialCapacity, 则table大小为threshold, 即大于指定
        //initialCapacity的最小的2的整数次幂
            
        // 从下面开始, 初始化table或者扩容, 实际上都是通过新建一个table来完成的
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        
        // 下面这段就是把原来table里面的值全部搬到新的table里面
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                // 这里注意, table中存放的只是Node的引用, 这里将oldTab[j]=null只是
                // 清除旧表的引用, 但是真正的node节点还在, 只是现在由e指向它
                    oldTab[j] = null;
                    
                    // 如果该存储桶里面只有一个bin, 就直接将它放到新表的目标位置
                    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
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

    3.resize时的链表拆分

    下面我们单独来看看这段设计的很精妙的代码

    //这里定义了四个Node的引用,从变量命名上,我们初步猜测,这里定义了两个链表,我们把它称为
    //lo链表和hi链表,loHead和loTail分别指向lo链表的头节点和尾节点,hiHead和hiTail以此类推
    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    //整个do...while循环就是循环数组元素上的链表
    do {
        //先获取链表当前节点的下一个节点
        next = e.next;
        //这里就是判断元素在新数组中的索引是原索引还是原索引+1
        //例如上面的key1的hash值(0 0101),key2的hash值(1 0101)
        //老数组长度n为16时,key&(n-1)的值key1和key2一样
        //但如果直接key&n,则一个为0,一个为16,这里就是以这个作为判断条件
        if ((e.hash & oldCap) == 0) {
            //这里和下面一个道理,第一次进来链表头节点指向e1,同时尾节点同样指向e1
            //第二次进来,尾节点的next指向e2同时再把尾节点指向e2,此时头节点loHead
            //还是指向e1,而尾节点loTail指向了e2,第三次同理,最后就是loHead永远指向e1,
            //而loTail指向en,n为链表长度
            if (loTail == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
        }
        else {
            if (hiTail == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
        }
    } while ((e = next) != null);
    //下面两个直接把不为空的链表放置于各自位置
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
    

    相关文章

      网友评论

          本文标题:HashMap源码之resize方法

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