之前有对HashMap的核心源码put流程进行过分析,今天来分析一下resize流程以及移动元素时计算索引的原理。
源码分析
这里对resize的主要流程进行分析,主要结合注释和流程图进行理解。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//当oldTab为null时设置oldCap旧容量为0;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//引用原有临界值
int newCap, newThr = 0;//初始设置新的容量和新的扩容上限为0
if (oldCap > 0) {
//超过最大值就不进行扩容操作,只能任随其进行碰撞
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//没有超过就扩容为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//当原有临界值大于0时表示已经初始化过了
else if (oldThr > 0) // initial capacity was placed in threshold
//如果已经初始过则将旧的扩容上限设置为新的容量
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//临界值<=0表示还没有进行初始化,使用默认的初始容量进行初始化
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
//计算新的resize上线
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//更新临界上限
@SuppressWarnings({"rawtypes","unchecked"})
//根据newCap进行生成table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//16大小的数组
table = newTab;//将新的table赋值给table属性
//将oldTab中的值赋值给newTab
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {//遍历所有元素
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//当前索引位置没有后续的节点,则直接将当前节点放入新的table对应的索引位置
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;
//这个地方很巧秒,通过e.hash & oldCap来得出newTab中的位置,
//因为table是2倍扩容,所以只需要看hash值与oldCap进行操作,结果为0,那么还是原来的index;否则index = index + oldCap
//至于为什么这样计算就可以判断索引位置我们后面具体分析
if ((e.hash & oldCap) == 0) {
//对lo链表添加新的节点
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//对hi链表添加新的节点
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//即e.hash & oldCap == 0的链表索, 引不会发生变化, 直接放入newTable中 原索引 位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//即e.hash & oldCap != 0的链表, 索引会发生变化, 在原索引 + oldCap位置, 放到newTable中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize流程图:如果大图不清晰的话可以通过processOn查看
索引计算原理
在遍历oldTab上的链表时进行了 (e.hash & oldCap) == 0 的判断,如果成立则索引位置不变,如果不成立则新的索引位置为 :
index = index + oldCap,如上面流程图中最右边部分的循环图示。
接下来我们就来证明一下为什么上面的计算方式是正确的,首先明白三个计算原则:
1.table的容量始终是2^x,每次扩容都是扩容为原有的2倍(特殊情已经在流程中过滤)
2.hash的运算:在HashMap中是使用了移位运算,h = key.hashCode()) ^ (h >>> 16), 即高16位和低16位进行亦或运算(“>>>”符号是无符号右移,忽略符号位,空位都以0补齐)
3.索引的计算公式为(n - 1) & hash,相当于取模运算 hash % n (注:这里的n为table的容量,具体原理可以参考博客)
对于二进制运算在这里我们就用符号"..."代替不重要的低位上的数了,高位我们用0补齐。
这里我们设置 oldCap = 2^x,我们用二进制表示 oldCap = 00000000 1(第x+1位) 000....000 (1后面一共x个0,即低x位都为0)
根据第三条计算原带入x计算原索引:index = (2^x - 1) & hash
计算结果:index = 00000000 0(第x + 1位) ..........(低x位) ;
那么 newCap = oldCap * 2 = 2^(x +1),
扩容后新索引为:newIndex = (2^(x + 1) - 1) & hash
则索引结果无非就是两种:在二进制中的 x+1 位不是0就是1
第一种结果:newIndex = 0000000 0(第x + 2位) 1 (第x + 1位) .........(低x位)
第二种结果:newIndex = 0000000 0(第x + 2位) 0 (第x + 1位) .........(低x位)
到这里我们就可以知道如果是第 x + 1 位是0那么和index的值就是一样的。如果第 x + 1 位是1那么得到:
newIdex = index + oldCap
即:0000000 0(第x + 2位) 1 (第x + 1位) .........(低x位) = 00000000 0(第x + 1位) ..........(低x位) + 00000000 1(第x+1位) 000....000
所以得到必然的结果就是新索引要么与原索引相同要么就是原索引加上原容量值得到新索引。
但是这还不足以解释为什么 (e.hash & oldCap) == 0 就可以判断新索引与原索引不变。
细心点你可以发现(e.hash & oldCap) = hash & 2^x,这个式子相当于 hash & 0000000 1(第x+1位) 000....000 (1后面一共x个0,即低x位都为0)
这里可以发现和求新索引的计算式很像: hash & (2^(x+1) -1),实际上就是将后面的低x位全部换成0,只需关心第x+1位进行hash运算后的值。这里我们得到了我们想要的结论:
hash & oldCap = 0,代表的就是第x + 1位上的值位0,hash & oldCap = 1,代表x + 1位上的值为1。
再根据前面的结论: x + 1 位为0时,newIndex = index;x + 1位为1时, newIndex = index + oldCap。
所以最后得出: (e.hash & oldCap) == 0,newIndex = index;(e.hash & oldCap) != 0, newIndex = index + oldCap