JDK1.8 HashMap扩容源码(resize()方法)解读
程序员文章站
2022-06-04 19:14:40
...
- 扩容源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//oldCap:旧数组长度,oldThr:旧扩容阈值,newCap:新数组长度,newThr:新扩容阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//旧数组非空时,若旧数组长度已经达到上限(2的30次方)则将扩容阈值设成2的31次方-1(Integer类型最大长度)并直接返回
//否则,当新数组长度小于上限且旧数组长度大于等于默认值(16)时,令新的扩容阈值=旧扩容阈值*2
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧数组为空而旧扩容阈值大于0时,令新数组长度等于旧扩容阈值
//这里要注意,当new出来一个空HashMap并自定义数组长度时,扩容阈值为大于自定义长度的2的最小次幂
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//旧数组为空且旧扩容阈值为空,则新数组长度为默认值16,新扩容阈值为16*0.75=12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新扩容阈值为空时重新计算新扩容阈值:若新数组长度小于上限且新数组长度*负载因子小于上限,则令新扩容阈值=新数组长度*负载因子,否则为Integer最大长度
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新扩容阈值赋予HashMap
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化新数组,从这里开始正式执行将旧数组中的数据放进新数组的过程
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历旧数组的每个Entry节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//节点中有数据则继续,没数据直接continue
//这里分了三种情况:1.节点e只有一条数据 2.节点e是红黑树 3.节点e是节点数大于1的链表
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//该节点的下一跳为空,即该节点只有一条数据:重新计算该节点的index(即key的hash值与上新长度-1)
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//该节点为红黑树:把树拆成一个低位树和一个高位树,低位树放在原来的下标处,高位树放在下标为旧下标+旧数组长度的下标出
//判定高低位的逻辑可以参考第三种情况:节点e是节点数大于1的链表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//该节点为节点数大于1的链表:遍历链表,同样把链表拆分成高位和低位两个链表,分别放在下标为j+oldCap和j处(oldCap:旧数组长度)
else { // preserve order
//这里定义了四个节点,实际是两个链表
//lohead、lotail分别是lo链表的头节点和尾节点,hihead、hitail同理
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//这里区分高位低位的逻辑:如果e的hash值与上就数组长度等于0则是低位,否则为高位
//这里打个比方,比如e的hash值是101111,旧数组长度是16即100000,结果就是100000,这里需要判断结果是否等于0,即只需要判断
//e.hash的第6位数组是否是0就可以了,即不论oldCap值是多少,只需要关注e.hash某一位的值是0还是1,就可以以此判定高低位
next = e.next;
if ((e.hash & oldCap) == 0) {
//尾插法
//这里可以这么理解:loHead指向头节点,loTail指向尾节点,loTail是随着节点增加不断向后移动的
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;
}
- 扩容的源码看着长其实不是很复杂大概分成两个部分:
- 计算新扩容阈值和新数组长度
- 把旧数组里的数据转移到新数组里(重点)
数据迁移过程只要重点关注把Entry节点(链表或红黑树)拆成两个部分,一部分填入原来的下标处,另一部分填入旧下标+旧数组长度的下标处的逻辑就不难理解了。