HashMap的扩容是学习HashMap源码的重中之重,面试中经常被问到。本文就以实例的方式,解析HashMap的扩容过程,以及JDK1.8和1.7的扩容操作的区别
一、什么时候HashMap会扩容
调用HashMap的put方法时,如果当前的数组(HashMap的底层数据结构就是数组)为null,或者数组的长度大于阈值(数组长度*负载因子)时,会发生扩容。数组为null时,会扩容成默认长度或指定长度;数组超过阈值时,会扩容成原数组的两倍。
二、怎么扩容
假设现在有一个HashMap,长度为5,往里面put了三个元素put(1, val1),put(6, val2),put(11, val3),如图:
扩容:
新数组的容量会扩大一倍,里面暂时没有元素,新数组示意图如下:
JDK1.7 元素位置计算
第一步:遍历到oldTab[1]的(11,val3)这个元素,算出应该放在新newTab[1],完成后结构如下:
第二步,遍历到oldTab[1]的(6,val2)元素,计算出应该放在newTab[6]的位置,完成后结构如下:
第三步,遍历到oldTab[1]的(1,val1)元素,计算出应该放在newTab[1]的位置,由于index=1的位置上,已经有一个元素了,
所以(1,val1)元素,将放在newTab[1]的链表头上,完成后的结构如下:
完成元素的位置计算,扩容也完成了。从图中可以看出,扩容后原tab[1]处的链表,头尾发生了交换。
JDK1.8 元素位置计算
这个就骚气了
第一步:遍历到oldTab[1]的(11,val3)这个元素,算出应该放在新newTab[1],发现index还是1,没有发生改变,loHead、LoTail会指向这个元素,newTab暂时没有发生变化
第二步,遍历到oldTab[1]的(6,val2)元素,计算出应该放在newTab[6]的位置,index发生了变化,hiHead、hiTail会指向这个元素,newTab还是没有变化。
第三步,遍历到oldTab[1]的(1,val1)元素,计算出应该放在newTab[1]的位置,发现index还是1,没有发生改变。这时,由于loHead、loTail已经不为null了(指向元素(11,val3)),会将loTail.next指向(1,val1),loTail也指向(1,val1),如图:
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
第四步,将oldTab[1]处理结束后,将loHead、loTail、hiHead、hiTail,挂到newTab执行的index上去。
源码中的处理如下,只需把loHead、hiHead放到正确的位置即可。其中loHead链表index没发生改变,然后放在newTab[1]上,hiHead发生了改变,应该放在j + oldCap位置上,例子中j=1,oldCap=5。:
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
完成元素的位置计算,扩容也完成了。从图中可以看出,扩容后原tab[1]处的链表,头尾保持了1在11的后面。这就是jdk1.8与1.7的区别之处。
补充:
1、jdk1.8中是如果判断元素应该存放的index,在扩容之后,是否应该发生改变?上源码
判断原来的 hash 值与oldTab容量按位与操作是 0 或 1 就行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组
2、扩容后,如果index发生了改变,应该放在newTab的哪个位置呢,上源码:
其中loHead是index不变的链表,还是放在原来的位置j上,hiHead是index发生变化的链表,放在了j+oldCap上。