本文所写博文是基于JDK1.8
1. 注释翻译
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* 初始化或翻倍扩大table。如果table为null,则根据存放在threshold变量中的初始化capacity的值来分配table内存。
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 如果table不为null,由于我们使用2的幂次方来扩容,则每个bin元素要不在原来的bucket桶中,要不在2的幂次方中。
* @return the table
*/
resize()方法的两个作用:
- 数组table为空时,根据存放在threshold变量中的初始化capacity的值来分配table内存。注:HashMap中没有capacity成员变量。
- 数组table不为空时,使用2的幂次方来扩容。
2. 源码剖析
从HashMap的默认构造方法初始化HashMap,我们知道它一开始并没有做任何操作,就只是将默认的负载因子(0.75f)赋给成员变量loadFactor。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 首次初始化时,table为null
// 保存table容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 首次添加元素且默认构造方法时,threshold为0
int oldThr = threshold;
int newCap, newThr = 0;
// table不为null,即table已经扩容过
if (oldCap > 0) {
// 当前table容量大于最大容量值时,因为要往map中放入元素,则需要将阈值设置为整数的最大值Integer.MAX_VALUE,直接返回oldTab的地址(这样容量就会使现有table的容量<threshold=capacity*loadFactor>会变大,可以继续存放元素)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容后,新容量(旧容量oldTab*2)小于最大容量 且 旧容量大于等于16(默认的容量)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 阈值也扩大1倍(threshold=capacity*loadFactor),因为capacity变为原来的2倍,负载因子loadFactor不变,则阈值threshold也要变为原来的2倍
newThr = oldThr << 1; // double threshold
}
// 这里代表使用了有参构造方法,初始化了threshold(this.threshold = tableSizeFor(initialCapacity))为1,
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// table超过,且没有使用有参构造方法
else { // zero initial threshold signifies using defaults
// 将默认的初始化容量赋值给新容量newCap,将新的阈值计算出来0.75f*16
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的阈值为0,
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"})
// 以新容量,开辟新的table用来存储KV
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// table不为null,即table已经扩容过
if (oldTab != null) {
// 将旧table中元素移动到新的table中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 当前位置桶中只有一个结点,直接将该结点计算hash值存放入新的table中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 当前桶中链表已经树化,将红黑树中结点拆分为较低和较高的树并验证拆分后的树结点小于等于6,会将红黑树解除树化,还原为链表。
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 {
// e是链表的一个结点
next = e.next;
// 这里的计算和我们putVal的方法不同putVal方法是hash&(cap - 1),这里是hash&cap!!!需注意
// 这个方法判断的是元素在数组中的位置是否需要移动,==0 则不需要移动。
if ((e.hash & oldCap) == 0) {
// loHead和loTail标记的是不需要移动的元素
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// hiHead和hiTail标记的是需要移动的元素
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;
}
3. resize()方法执行流程
- 判断哈希表是否已经初始化,若还未初始化,根据初始化容量进行初始化操作
- 若哈希表已经初始化,将原哈希表按照2倍的方式扩容
- 扩容后进行原表元素的移动
a. 若桶中结点已经树化,调用树的方式移动元素(若在移动的过程中发现红黑树结点<=6,会将红黑树解除树化,还原为链表)
若还未树化,调用链表的方式来移动元素
4.流程图
5. 总结
- resize()方法是实现初始化table和扩容的方法。
- 不考虑极端情况,容量理论最大极限有MAXIMUM_CAPACITY指定,数值为1<<30,
- 阈值=负载因子*容量,如果构建HashMap的时候没有指定它们,那么就是一句相应的默认常量值。
- 阈值通常是以倍数进行调整的(例如:newThr = oldThr << 1),这个理论是在putVal中提过的,当元素个数超过阈值大小,则调整Map大小。
- 扩容后,需要将老的数组中的元素重新放置到新数组中,这是扩容的一个主要的开销来源。