final Node
resize() 方法的作用是对哈希表进行初始化或者扩容。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 对原table的「容量oldCap」、「阈值oldThr」状态进行判断,并确定新 table 的「容量 newCap」、「阈值 newThr」
// 原 table 「容量」大于 0
if (oldCap > 0) {
// 若 oldTab 的「容量oldCap」大于最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE; // 将「阈值oldThr」设置为整数最大值
return oldTab; // 直接返回
}
// 若 oldTab 扩容后的「容量newCap」仍在合理范围内且不小于默认的初始容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold // 将「阈值oldThr」扩大一倍得到 「newThr」
}
// oldTab「容量oldCap」等于 0,但是「阈值oldThr」大于 0
// (table 容量小于0会抛出异常的,所以一定是等于0)
else if (oldThr > 0)
newCap = oldThr; // 将 oldTab 「阈值oldThr」赋值给 newTab 的「容量newCap」
else {
// 对未初始化的 table 进行初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 对于 「newThr」 未赋值的情况进行处理
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE); // 若ft在合理范围内,将其赋给 newThr,否则newThr赋值为整数最大值
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 新建 newTab
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 若 oldTab 不为null,对 oldTab 里的元素进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 取出当前桶里的元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 将 oldTab 的这个位置置为空(暂时不理解是否有必要这样做)
if (e.next == null)
// 如果桶里只有一个元素,直接重新计算该元素在 newTab 的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果桶里的元素是红黑树形式的,将其拆分rehash
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 若桶里不止一个元素,且是以链表形式存在的
Node<K,V> loHead = null, loTail = null; // 前缀为lo的变量指向的是rehash后新旧Tab位置一样的链表,loHead和loTail分别指向链表头部和尾部
Node<K,V> hiHead = null, hiTail = null; // 前缀为hi的变量指向的是rehash后新旧Tab位置不一样的链表,hiHead和hiTail分别指向链表头部和尾部
Node<K,V> next;
do {
next = e.next;
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);
// 将链表放进 newTab 中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将链表放进 newTab 中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
先梳理一下 resize 的整个流程
先判断 oldTab 的状态,然后确定 newTab 的容量和阈值
之后,将 oldTab 桶里的元素放入到 newTab 中。
那 resize 函数是如何计算每个元素在 newTab 的位置的呢?
首先我们要了解一下关于 capacity 的两个特性
再者,每个元素 e 在 table 中的位置是通过e.hash & (Capacity - 1)来计算的,这样可以使每个元素都落在table里,不会溢出。简单举个例子:
e.hash 25 : 1 1 0 0 1 17 : 1 0 0 0 1
cap - 1 7 : 0 0 1 1 1 7 : 0 0 1 1 1
result & : 0 0 0 0 1 & : 0 0 0 0 1
index 1 1
因此,由于 capacity 都是 2 的 n 次幂 的这个特性,无论元素的hash值有多大,与(capacity-1)做 & 运算后,得到的结果都是 0~n-1,即只取后 log~2(capacity) 位。
那如果将newCap扩大一倍呢,结果就会是这样。
e.hash 25 : 1 1 0 0 1 17 : 1 0 0 0 1
newCap-1 15 : 0 1 1 1 1 15 : 0 1 1 1 1
result & : 0 1 0 0 1 & : 0 0 0 0 1
index 9 1
由于哈希表扩容 newCapacity = 2 * oldCapacity,在以二进制的形式表示时,将 oldCapacity 向左移一位,低位补0, 就可以得到 newCapacity 了,所以(oldCapacity-1)向左移一位,低位补1,就可以得到(newCapacity-1)。
在下图所示的例子中,e.hash 分别与 oldCapacity 和 newCapacity 做与运算时,capacity-1的二进制表示中后三位都一样,只有第四位不一样。
这样的话,我们在计算元素 e 在 newTab 的新位置时,只需要查看第四位是 0 还是 1 就可以了。
这里就可以解释为什么代码中有前缀为 lo 和 hi 的指针了,一个指向位置不变的链表,一个指向位置变化的链表(因为位置变化的元素在 newTab 中的位置都是一样的,都是 indexOfOldTab + oldCapacity)
在上面的例子中,对e.hash 的第四位进行判断,只需要 e.hash&oldCapacity 就可以了,因为 capacity 都是2的n此幂。
25 : 1 1 0 0 1 17 : 1 0 0 0 1
8 : 0 1 0 0 0 8 : 0 1 0 0 0
& : 0 1 0 0 0 & : 0 0 0 0 0
至此,resize() 函数的整个流程和思想基本就能看懂了。如果哪里写的不清楚,或者对这个函数有别的疑问,欢迎留言一起交流~
-》挖个坑,下次写 resize() 函数中对红黑树进行处理的函数,可是我连红黑树都没弄明白 = 。=
-》想建个技术联盟或者源码联盟,有兴趣的可以通过公主号联系我呀:一起学数据结构与算法。期待各位大佬的加入!!!