HashMap源码之resize()详解

本文详细探讨了HashMap在Java 1.8中的resize()方法,包括四种扩容情况、新表长度和阈值的计算,特别是对`if (newThr == 0)`条件的解释。在数据迁移部分,文章分析了红黑树和链表的处理方式,以及如何通过位运算优化定位元素的过程。

请配合源码食用本文
jdk版本:1.8

本文重点:

if (newThr == 0)的原理

进入正文:

四种扩容情况

(oldCap、oldThr、newCap、newThr 指:旧表长度、旧表阈值、新表长度、新表阈值)

  1. 旧表已初始化了 (oldCap>0)

    1. 旧表长度 >= 230 ,无法再扩容,将阈值threshold设为int最大值,直接返回

    2. 默认初始容量(24)<= 旧表长度 < 229 (右边不取等号,那229 怎么办?)

      • 此时正常扩容:长度、阈值都翻倍(*2)

      • 注意这里的实现:

        if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                             oldCap >= DEFAULT_INITIAL_CAPACITY)
                        newThr = oldThr << 1; 
        

        (newCap = oldCap << 1) 这种实现保证了无论条件是否成立,新表长度都等于旧表长度的两倍,但新阈值则符合条件才能翻倍

  2. 旧表未初始化,但旧阈值(oldThr)>0 。对应情况:new时指定了initialCapacity

    - 操作:  新表长度 = 旧阈值 (newCap = oldThr)
    
  3. 其余情况,指未初始化,旧阈值也为0。最常见,因为对应情况:无参构造

    /* 经典操作 */
    newCap = DEFAULT_INITIAL_CAPACITY;   // 16
    newThr = 
     (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75f=12
    

if (newThr == 0)

分不同情况设置好新表长度、新阈值后,在数据迁移前,还需要一步:

 if (newThr == 0) {
    // loadFactor正是通过影响newThr,进而影响resize
    float ft = (float)newCap * loadFactor;   
    newThr = (newCap < MAXIMUM_CAPACITY 
           && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
        }

为什么需要这个判断?其实这里大有玄机,对应了三种情况

  1. 扩容中的1.2情况中,当旧表长度不符合条件时,新阈值并没有被赋值

  2. 扩容中 2 情况中,只设置了新表长度,并没有设置新阈值

  3. 扩容1.2情况,符合条件,但新阈值扩容时( oldThr << 1),最高位被移除,变为0。

    • 这是一种很特殊的情况,但可能发生,只要令loadfactor为2的幂次方,就可以满足条件,eg:初始容量为226,负载因子为23

      过程:初始化时,只设置阈值threshold。当put后触发分配空间,先进入 2 情况,令 newCap = oldThr ; 再进入上述if , 令 newThr = newCap * loadfactor。

      后续就是正常扩容

      oldCapnewCapoldThrnewThr
      初始参数002260
      第"零"次扩容0226226229
      第一次226227229230
      第二次227228230231(负数)
      第三次 3.12282292310
      3.2:进入上述if同上同上同上Integer.MAX_VALUE
      第四次229230Integer.MAX_VALUEInteger.MAX_VALUE

      可能有细心的读者觉得,3.2情况下会不会有两种情况:ft非法跟合法

      其实不会的,因为ft是float型,范围远比int大,所以只要是这种溢出的情况,

      ft < (float)MAXIMUM_CAPACITY 就不可能会成立。

值得一提的是,第2-4次扩容几乎是前后一起发生的。因为put操作中判断扩容的条件是:

 if (++size > threshold)   resize();

因为第二次扩容后,(threshold = newThr) 为负数,再put一次又再次resize,然后又为0,再put一次又会触发扩容。

说明一下,这三种情况中,1、2笔者都测试过无误,但笔者电脑有点渣,测试第三种情况会GC频繁异常,无法保证正确,也查找不到相关资料。希望有读者测试后能反馈一下是否正确。


数据迁移

好了,终于来到最后一步,数据迁移。

这里比较简单:三种情况

  1. if (e.next == null)   newTab[e.hash & (newCap - 1)] = e;
    // 这种就是只有一个Node,还没形成两个及以上的链表,直接找个位子放
    
  2. 按红黑树处理、拆分

  3. 按链表处理,将节点分组放不同的位置

这里比较核心的是分组条件:

if ((e.hash & oldCap) == 0) { //放原位置}
   else { //放 (原位置+oldCap)  位置 }

对比一下之前定位元素都是用 (len-1)&e.hash ,
这里没有 -1,为什么?

  • 按照剧本的话,因为我们扩容了, newCap = oldCap << 1 ,此处应该是

(newCap -1)&e.hash

  • 但其实效果是等价的。因为e.hash是不变的,

若当前位置为j,则必定满足e.hash&(oldCap-1) = j , 那扩容前后,该等式都是成立的。那区别在哪?

没错,就在扩容了之后多出来的那一位上,而这一位就是 oldCap对应的二次幂的位。
eg: oldCap = 16 =24=1000(二进制)
e.hash两个例子中24位上分别为0、1

oldCap01 000001 0000
newCap10 000010 0000
e.hash00 0101←;→01 0101
e.hash&(oldCap-1)00 010100 0101
e.hash&(newCap-1)00 0101←;→01 0101

这样就只需比较一位(因为oldCap一定是二的幂),而不用再取hash值的低位了加快了运算速度

img
本文完。
最后膜拜源码大佬,Tql。

参考资料:
面试必会之HashMap源码分析
一文读懂HashMap

若有误,欢迎指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值