hashmap在resize(扩容)的过程,需要创建一个新的桶数组,然后要将旧的桶数组中的数据重新分配的新的桶数组,其中有个旧数据在新桶数组寻址的过程,代码如下:
// 当node的hash值 & 旧容量位 == 0时,这个数据是不需要换桶位置的
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 当hash值 & 旧容量位 != 0时,这个数据是需要换位置的,而且换的位置为:旧桶的位置 + oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
在网上看了好多关于这个问题的解读,都是从二进制层面去枚举的,看着比较不太容易理解,这里我结合jdk1.7的取模%算法来分析一下,方便同学们理解。
1.hashmap容量与扩容机制
hashmap的容量(桶数组长度)都是2的n次幂(1 << n),用二进制表示为00000000 00000000 00000000 00001000**,4个字节.太长了,我们假设高位的3个字节全为0,举例只截取低位的1个字节,初始容量为8,即00001000,右数第4位为1。当扩容时,会将容量左移一位即00010000,右数第5位为1,容量变为16.结论:hashmap的容量用二进制表示时,始终只有一位为1,其余的全为0.
2.jdk1.7桶寻址
这里就不在去寻找jdk1.7的源码了,具体思路就是取余,即%,取到几就是几号桶.还是以初始容量为8举个例子,这里都是以尾插举例:
上图枚举了4个hash值,仅仅只有当d的值变了.让我们看下规律.当cap=8时,cap也就是数组的长度.a、b、c、d这4个key从数据上看可以分为一下3类:
- a、b可以看第一类,因为a.hashcode = 1,b.hashcode = 5. 均小于8,当hashmap扩容后cap = 16,不管hashmap扩容几次.再怎么取模还是hashcode本身,不用换桶.
- c可以看第二类,因为c.hashcode = 17.虽然 大于8,但是当hashmap扩容后cap = 16,17对16取模之后还是为1,因为17为8的2倍还要多1,一次可以类推.hashcode值比当前的cap 大 (2n * cap)倍的,桶也不用换.
- d可以看错第三类,d.hashcode = 14 为 (1 * cap) + 6,所以他是需要换桶的.
以此可以类推:
结论:从上图可以看出容量从8扩容到16后只有**(2n+1)oldCap到(2n+2)*oldCap-1**位置的数据需要换桶;黑色不用换桶,红色需要换桶且新桶位置 = 旧桶位置 + oldCap.
ps:从上面的结论来看这是不是也可以作为hashmap的容量必须为2的n次幂(1 << n)的一个依据呢?因为只需一半的数据换桶.
3.jdk1.8桶寻址
// 其中n就是cap,就是将cap - 1 & hashcode.结果与对cap取余一样,看下表
Node p = tab[i = (n - 1) & hash]
key | hascode | cap | &(按位与) |
---|---|---|---|
a | 1(0000 0001) | 7(0000 1111) | 0000 0001 0000 1111 & 0000 0001 (1) |
b | 5(0000 0101) | 7(0000 1111) | 0000 0101 0000 1111 & 0000 0101 (5) |
c | 17(0001 0001) | 7(0000 1111) | 0000 0101 0001 0001 & 0000 0001 (1) |
d | 14(0000 1110) | 7(0000 1111) | 0000 1110 0001 1111 & 0000 1110 (14) |
然后我们再来看二进制的结构:
-
高4位为 2^4 = 16, 而16 % 16(新容量) = 16 % 8(旧容量) = 0;即比旧容量位高的二进制位可以忽略,对取余的结果起不到任何作用。(当然高4位可以为任意值,但总是16的倍数, 取余均为0)。
-
低3位为 2^0 = 1,而 1 % 16(新容量) = 1 % 8(旧容量) = 1;即低3位对新容量、旧容量取余结果一样,也起不到作用.
(大家可以尝试下穷举1-7,发现结果一样)。 -
结果发现能左右是否换桶的只有与旧容量相同的位(旧容量位,右数第四位),才能起到作用.
(1)当hashcode=17时旧容量位为0,即不换桶.
(2)当hashcode=25时旧容量位为1,新桶位置为旧桶位置 + oldCap,或者为旧桶位置 + (key.hashcode & oldCap).
我们再来看下jdk1.7得出的结论,然后你会惊奇的发现
- 当hashcode对应的旧容量位为1时,这个值刚好落在(2n+1)*oldCap -> (2n+2)*oldCap-1之前.比如25,其中oldCap=8,得出n=1.落在图二的红色位置
- 当hashcode对应的旧容量位为0时,这个值刚好落在(2n*oldCap) -> (2n+1)*oldCap-1.比如17,其中oldCap=8,得出n=1.落在图2的黑色位置.
结果我们发现,hashmap由原来的 取余找桶 %,到现在的 **按位与 &**找桶,除了形式变了,其中处理的逻辑变了吗?