HashMap浅析(二)
扩容机制
首先上扩容的代码,每一行都写了注释,尽可能的让大家明白这个过程。
final Node<K,V>[] resize() {
//将table赋值给oldTab
Node<K,V>[] oldTab = table;
//声明oldTab的容量给oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容阈值赋值给oldThr
int oldThr = threshold;
//上面这三行代码,其实就是为了区分出扩容前与扩容后的变量,众所周知,数组无法动态扩容
//声明新数组的容量和扩容阈值,暂时为0
int newCap, newThr = 0;
//如果旧数组的容量大于0
if (oldCap > 0) {
//如果旧数组的容量大于等于最大值,即1<<30
if (oldCap >= MAXIMUM_CAPACITY) {
//全局变量的扩容阈值赋值为int的最大值,同时返回旧数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果新数组的容量(旧数组容量*2)小于最大容量,且,旧数组容量大于等于默认值1<<4
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新数组的扩容阈值等于旧数组扩容阈值的2倍
newThr = oldThr << 1; // double threshold
}
//如果旧数组的扩容阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//新数组的扩容阈值就等于旧数组的扩容阈值
newCap = oldThr;
//如果既不走oldCap>0,也不走oldThr>0的分支,那么走else
else { // zero initial threshold signifies using defaults
//新数组的容量为默认容量:16
newCap = DEFAULT_INITIAL_CAPACITY;
//新数组的扩容阈值=默认载荷因素*默认初始容量=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的if分支:如果新数组的扩容阈值等于0
if (newThr == 0) {
//ft=新数组容量*0.75,loadFactory在用户未指定时,就是默认的0.75
float ft = (float)newCap * loadFactor;
//当新数组的容量小于最大容量,且ft小于最大容量时,新数组的扩容阈值就为ft,
//否则为int的最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新得到的新数组的扩容阈值,赋值给全局变量的扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个长度为newCap的node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//并让全局变量的table数组指向新数组
table = newTab;
//如果旧数组不等于空
if (oldTab != null) {
//循环旧数组
for (int j = 0; j < oldCap; ++j) {
//声明一个Node对象,用来承载旧值
Node<K,V> e;
//将数组的j角标位置的值赋值给e,同时判断是否为null
if ((e = oldTab[j]) != null) {
//不为空,则把原数组的j位置重新赋值为null
//当循环完毕,旧数组就是空数组,方便回收
oldTab[j] = null;
//判断当前数组元素e的链表的下一个结点是否为null
if (e.next == null)
//证明当前数组元素只有一个结点,所以只需在新数组中放置e元素
newTab[e.hash & (newCap - 1)] = e;
//首先走到else if分支这里,已经确定e.next不为null
//那么就需要判断是红黑树还是链表
else if (e instanceof TreeNode)
//红黑树部分先过掉
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//确定为链表之后
else { // preserve order
//创建五个node对象
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//注意是do-while循环,即先执行一次,再走while
do {
//next即为当前遍历到的元素的下一个结点
next = e.next;
//如果当前元素的hash&旧数组容量的值为0
//当你看到这里时,请先看下一节:扩容中链表复制的详解
//便于理解之后的流程
//判断角标是否需要修改,下面的分析会单独拿出来,因为涉及到循环和判断
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
do-while中具体执行流程
现在假设当前元素e的链表中存在三个结点,a、b、c分别为三个结点的内存地址,假设e不需要重新计算角标
也就是e ->a, e.next->b ,e.next.next->c
第一次执行
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//第一次执行,next->b,e->a
next = e.next;
//判断元素位置是否改变
if ((e.hash & oldCap) == 0) {
//不需要改变走这个分支
//判断loTail是否为null,现在是第一次执行,肯定是null,进入if分支
if (loTail == null)
//loHead->a
loHead = e;
else
loTail.next = e;
//loTail->a
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//然后执行判断的同时,将e->next,e->b
} while ((e = next) != null);
用个图表示第一次执行之后,变量的关系
第二次循环
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//next->c,e->b
next = e.next;
if ((e.hash & oldCap) == 0) {
//loTail->a,所以走else
if (loTail == null)
loHead = e;
else
//loTail.next->b
loTail.next = e;
//loTail->b
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//然后执行判断的同时,将e->next,e->c
} while ((e = next) != null);
第三次循环
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//next->null,e.next->null,e->c
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
//loTail.next->c
loTail.next = e;
//loTail->c
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//e=next == null,while循环结束
} while ((e = next) != null);
跳出循环之后的处理
//loTail不为null,因为loTail指向的是a的内存地址,而a又是链表的头结点
//所以只需要将新数组的j位置元素指向loHead的地址,最终也就是指向的a
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//这个方法是针对需要重新计算元素下角标的元素
if (hiTail != null) {
hiTail.next = null;
//针对为何是j+oldCap可以查看下面的内容
newTab[j + oldCap] = hiHead;
}
扩容中链表复制的详解
在resize()方法中,针对链表的复制,HashMap采取了一个非常有意思的方法。
它没有针对新数组重新计算hash,从而确定下角标,而是通过e.hash & oldCap判断元素的前后位置是否有变化。
-
假设当前元素的hash为10:1010(二进制),数组扩容前为8:1000(二进制),
那么计算角标的方法就是(8-1)&10,index=2
-
扩容后的下角标,hash为10:1010(二进制),数组扩容后为16:0001 0000(二进制),
那么计算角标的方法就是(16-1)&10,index=10
大家仔细看,其实扩容就是高一位从0到1,这时如果确定hash的对应位是0还是1,就可以确定,元素在新数组的位置是否有变化了。
那么如何确定呢,e.hash & oldCap就是用来判断这个变化的。
首先&的运算,就保证了如果两个二进制的对应位都是1,计算出来的值的对应位才为1.
由上图可知,e.hash & oldCap!=0,所以需要重新计算位置。
通过下图,你能发现什么问题呢?
新旧座标的差值其实就是数组扩容前后,区分出来的高一位1,然后又因为数组扩容是严格按照2的幂次方进行的,所以必然存在:
当e.hash & oldCap!=0时,
新数组的下角标位置=旧数组的下角标+2的m次方(m=图中容量扩容前后-1的二进制码中的发生变化的位置)
而
所以有
如果还不够明白的话,我继续通过推导e.hash & oldCap==0的情况给各位再反向操作一波。
为了方便推导,我将其中的hash值修改了一下,下图是扩容前后求index的图。
下图可知:index值在扩容前后没有发生变化。
下图是e.hash & oldCap的计算图,对应图中标注的1.2.3,依次做一个解释。
1.因为容量是严格按照2的幂次方扩容,所以整段二进制码中,只存在一个1;
2.两者进行"&"计算后,如果hash的对应位(指容量的二进制码中1的位置)为0,那么结果必然为0.
3.反之亦然。
当扩容后的容量为16, 0001 0000,
所对应的n-1,即16-1, 0000 [1]111,
旧容量的n-1,即8-1, 0000 [0]111,
二者的区别就在[]号标识的那一位,所以利用上面2.的结论,无论是扩容多少次,满足e.hash & oldCap==0的条件下,角标都不会变