HashMap的扩容机制(JDK1.8)

HashMap扩容方法resize()源码:

//HashMap允许的最大容量,我理解就是数组的最大长度,而不是键值对总数
static final int MAXIMUM_CAPACITY = 1 << 30;
//数组默认初始长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//默认的加载因子 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final Node<K,V>[] resize() {
        //把当前HashMap的数组赋值给oldTab,顾名思义,oldTab就是老数组了
        Node<K,V>[] oldTab = table;
        //取得老数组的长度,为null说明HashMap还没完成初始化,返回0,否则返回数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //把当前HashMap的阈值赋值给oldThr,顾名思义,这个oldThr就是老阈值了
        int oldThr = threshold;
        //定义新的数组长度newCap,新的阈值newThr,默认为0
        int newCap, newThr = 0;
        //如果oldCap大于0,说明老数组至少已经初始化完成了
        if (oldCap > 0) {
            //如果老数组的长度oldCap大于等于MAXIMUM_CAPACITY,说明数组长度已经超过最大限制2的30次方了
            if (oldCap >= MAXIMUM_CAPACITY) {
                //就不会再扩大数组了,直接把Integer.MAX_VALUE赋值给阈值,Integer.MAX_VALUE=2^31-1
                threshold = Integer.MAX_VALUE;
                //然后返回旧数组,这步可以理解成数组已经无法再扩大了,只能扩大能容纳的键值对总数了
                return oldTab;
            }
            //否则就把旧的数组长度oldCap左移一位,也就是乘以2赋值给新数组长度newCap,同时还要判断新数组长度newCap要小于MAXIMUM_CAPACITY以及旧的数组长度oldCap不能比默认初始数组长度DEFAULT_INITIAL_CAPACITY(16)小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //同时旧的阈值也要乘以2,然后赋值给新阈值     
                newThr = oldThr << 1; // double threshold
        }
        //如果老数组的长度oldCap并没有大于0,说明还没做初始化操作,但是这时它的旧阈值oldThr却大于0,这说明了构造HashMap时传入了初始容量,而HashMap会根据传入的初始容量来定义阈值,这里给出部分代码参考:this.threshold = tableSizeFor(initialCapacity),而数组的初始化其实是在put()方法里才完成的,这也就能理解为什么有阈值而数组却还没初始化了
        else if (oldThr > 0)
            //那就把阈值值赋值给代表新数组长度的变量newCap
            newCap = oldThr;
        //走到这步,就说明旧阈值和旧数组都还没做初始化,说明调用的是HashMap无参的构造函数    
        else {            
            //初始化新数组长度newCap为DEFAULT_INITIAL_CAPACITY(16)
            newCap = DEFAULT_INITIAL_CAPACITY;
            //初始化新阈值newThr为(加载因子*默认容量)=0.75*16=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果新阈值等于0,其实可以发现上面大部分判断的代码块里都有设置newThr的值,只有else if (oldThr > 0){}这个判断里没有设置newThr的值,其他的要么早就有值,要么就做设值操作
        if (newThr == 0) {
            //新数组长度newCap*负载因子loadFactor的值赋值给ft
            float ft = (float)newCap * loadFactor;
            //然后判断下新数组长度newCap是否超过最大容量,同时ft的值如果超过MAXIMUM_CAPACITY,则设置为Integer.MAX_VALUE,否则返回ft
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //把新阈值newThr赋值给HashMap里代表阈值的属性字段threshold
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //创建一个长度为newCap的新数组,可以理解成,如果是在做初始化数组操作的话,那这就是初始化的数组,如果是在做扩容操作的话,那这就是新数组,是要用来扩容的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //把新数组赋值给HashMap里代表数组的属性字段table
        table = newTab;
        //如果旧数组不是null,说明要扩容,那接下来就要把旧数据移动到新数组里了
        if (oldTab != null) {
            //开始循环旧数组,oldCap是旧数组长度
            for (int j = 0; j < oldCap; ++j) {
                //定义一个变量e
                Node<K,V> e;
                //取得当前下标的数组节点,赋值给e变量,并判断是否为null 
                if ((e = oldTab[j]) != null) {
                    //不为null,说明旧数组在这个下标里有值,先把旧数组的这个下标位置的引用设置为null,方便GC时回收
                    oldTab[j] = null;
                    //如果e的后继节点为null,说明还没拉出链表来,这个桶里就一个节点
                    if (e.next == null)
                        //那就通过[e.hash & (newCap - 1)]计算出对应的新数组下标位置,并赋值上e节点,其实[e.hash & (newCap - 1)]操作就是对newCap做取余操作,只是用按位与运算效率会高很多
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果e节点是TreeNode类型,说明结构已经是红黑树了 
                    else if (e instanceof TreeNode)
                         //那就调用split()方法做扩容
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //否则的话就是链表结构
                    else { 
                        //设置低位首节点和低位尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //设置高位首节点和高位尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        //定义一个Node类型的变量next
                        Node<K,V> next;
                        do {
                            //取e的后继节点赋值给e变量
                            next = e.next;
                            //这里其实就是做一个按位与运算,具体怎么算,下面我会给个🌰
                            if ((e.hash & oldCap) == 0) {
                                //如果低位尾节点为null的话,说明还没开始遍历这个桶下的链表,就把e赋值给低位首节点
                                if (loTail == null)
                                    loHead = e;
                                //否则低位尾节点不为null的话,说明已经在遍历了  
                                else
                                    //把低位尾节点的后继节点设置为e节点
                                    loTail.next = e;
                                //把e节点赋值给低位尾节点,因为每次e节点都会被赋值成next,而原来的e又被赋值成loTail,通过loTail.next = e,就可以让e的后继节点指向e.next,所以这步加上上一步,就可以形成一个单向链表了
                                loTail = e;
                            }
                            //如果e节点的hash值对oldCap取余不等于0,说明这个节点是下标0之外的数组节点
                            else {
                                //这时判断高位尾节点是否为null
                                if (hiTail == null)
                                    //如果高位尾节点为null的话,说明还没开始遍历这个桶下链表,就把e赋值给高位首节点
                                    hiHead = e;
                                else
                                //如果高位尾节点不为null的话,说明已经在遍历了
                                    hiTail.next = e;
                                //把e节点赋值给高位尾节点,因为每次e节点都会被赋值成next,而原来的e又被赋值成hiTail,通过hiTail.next = e,就可以让e的后继节点指向e.next,所以这步加上上一步,就可以形成一个单向链表了    
                                hiTail = e;
                            }
                          //这里的next=e.next,循环直到没有下一节点为止
                        } while ((e = next) != null);
                        //如果低位尾节点不为null
                        if (loTail != null) {
                            //这个低位尾节点也没有后继节点了
                            loTail.next = null;
                            //就把首节点赋值给新数组下标为j的桶,和旧数组的位置是一样的,也就是说节点原来对应在旧数组的哪个下标,在新数组也不变。这里说点抽象的,把首节点赋值给新数组的桶,其实不单单只是首节点,因为每个节点都会有指针指向后继节点,所以其实可以想成是直接拉了一个链表到这个数组的某个桶里了。
                            newTab[j] = loHead;
                        }
                        //如果高位尾节点不为null
                        if (hiTail != null) {
                            //这个高位尾节点也没有后继节点了 
                            hiTail.next = null;
                            //就把首节点赋值给新数组下标为【j+oldCap】的桶,和旧数组的位置j相比,这里多偏移了旧数组长度oldCap个位置,变成了【j+oldCap】
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

e.hash & oldCap的🌰

首先要知道oldCap的长度都是2的次幂,比如16,32,转换成二进制的话,它的有效最高位是1,低位都是0,拿16做个🌰:

0000 0000 0000 0000 0000 0000 0001 0000
如果oldCap都是这样只有有效最高位是1,其余都是0的二进制的话,那么其实e.hash的二进制真正能够参与到运算的有效位数就是oldCap的最高位到最低位的位数,比如oldCap是16的话,那e.hash的真正有效位数就是5位

下面来做个按位与运算,只有相同的二进制数位上,都为1才是1,否则都是0:
1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 0000 -----16的二进制


0000 0000 0000 0000 0000 0000 0000 0000 —>0

再换个e.hash
1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 0000 -----16的二进制


0000 0000 0000 0000 0000 0000 0001 0000 ---->16

通过以上两个运算可以发现,只有e.hash与oldCap对应的有效高位上的值是1,运算结果才不为0,否则都是0。

数组长度之前为16,所以看下以上的两个e.hash对数组长度取模e.hash & (16-1)获取索引的结果是多少:
1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0000 1111 -----(16-1)的二进制


0000 0000 0000 0000 0000 0000 0000 1111 ---->15

1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0000 1111 -----(16-1)的二进制


0000 0000 0000 0000 0000 0000 0000 1111 ----15

这是假设数组长度已经扩容成了32,把以上的两个e.hash对数组长度取模e.hash & (32-1))获取索引的结果是多少:

1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 1111 -----(32-1)的二进制


0000 0000 0000 0000 0000 0000 0000 1111 ---->15

1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 1111 -----(32-1)的二进制


0000 0000 0000 0000 0000 0000 00001 1111 ----> 31

通过上述运算就可以发现,e.hash与oldCap对应的有效高位上的值是0的话,哪怕扩容了,新数组索引还是不变,还是15,而如果e.hash与oldCap对应的有效高位上的值是1的话,那这个元素在新数组的下标位置就等于【原数组下标位置+原数组长度】=【15+16】=31。这样就通过这个运算把原来的一条链表拆成了两条链表,然后这两条链表各自归属到新数组中对应的位置。

总结

阅读到这,已经能够大致了解HashMap的扩容机制了,可以发现JDK1.8没有再像JDK1.8前一样再重新rehash了,效率自然是提高了很多。

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值