请配合源码食用本文
jdk版本:1.8
本文重点:
if (newThr == 0)的原理
进入正文:
四种扩容情况
(oldCap、oldThr、newCap、newThr 指:旧表长度、旧表阈值、新表长度、新表阈值)
-
旧表已初始化了 (oldCap>0)
-
旧表长度 >= 230 ,无法再扩容,将阈值threshold设为int最大值,直接返回
-
默认初始容量(24)<= 旧表长度 < 229 (右边不取等号,那229 怎么办?)
-
此时正常扩容:长度、阈值都翻倍(*2)
-
注意这里的实现:
if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1;(newCap = oldCap << 1) 这种实现保证了无论条件是否成立,新表长度都等于旧表长度的两倍,但新阈值则符合条件才能翻倍
-
-
-
旧表未初始化,但旧阈值(oldThr)>0 。对应情况:new时指定了initialCapacity
- 操作: 新表长度 = 旧阈值 (newCap = oldThr) -
其余情况,指未初始化,旧阈值也为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.2情况中,当旧表长度不符合条件时,新阈值并没有被赋值
-
扩容中 2 情况中,只设置了新表长度,并没有设置新阈值
-
扩容1.2情况,符合条件,但新阈值扩容时( oldThr << 1),最高位被移除,变为0。
-
这是一种很特殊的情况,但可能发生,只要令loadfactor为2的幂次方,就可以满足条件,eg:初始容量为226,负载因子为23 。
过程:初始化时,只设置阈值threshold。当put后触发分配空间,先进入 2 情况,令 newCap = oldThr ; 再进入上述if , 令 newThr = newCap * loadfactor。
后续就是正常扩容
oldCap newCap oldThr newThr 初始参数 0 0 226 0 第"零"次扩容 0 226 226 229 第一次 226 227 229 230 第二次 227 228 230 231(负数) 第三次 3.1 228 229 231 0 3.2:进入上述if 同上 同上 同上 Integer.MAX_VALUE 第四次 229 230 Integer.MAX_VALUE Integer.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频繁异常,无法保证正确,也查找不到相关资料。希望有读者测试后能反馈一下是否正确。
数据迁移
好了,终于来到最后一步,数据迁移。
这里比较简单:三种情况
-
if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 这种就是只有一个Node,还没形成两个及以上的链表,直接找个位子放 -
按红黑树处理、拆分
-
按链表处理,将节点分组放不同的位置
这里比较核心的是分组条件:
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
| oldCap | 01 0000 | 01 0000 | |
|---|---|---|---|
| newCap | 10 0000 | 10 0000 | |
| e.hash | 00 0101 | ←;→ | 01 0101 |
| e.hash&(oldCap-1) | 00 0101 | 00 0101 | |
| e.hash&(newCap-1) | 00 0101 | ←;→ | 01 0101 |
这样就只需比较一位(因为oldCap一定是二的幂),而不用再取hash值的低位了加快了运算速度

本文完。
最后膜拜源码大佬,Tql。
参考资料:
面试必会之HashMap源码分析
一文读懂HashMap
若有误,欢迎指出。
本文详细探讨了HashMap在Java 1.8中的resize()方法,包括四种扩容情况、新表长度和阈值的计算,特别是对`if (newThr == 0)`条件的解释。在数据迁移部分,文章分析了红黑树和链表的处理方式,以及如何通过位运算优化定位元素的过程。
1123

被折叠的 条评论
为什么被折叠?



