`final Node<K,V>[] resize() { // 获取旧哈希表(Node数组) Node<K,V>[] oldTab = table; // 获取旧哈希表的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取旧哈希表的阈值(容量与负载因子的乘积) int oldThr = threshold; // 定义新哈希表的容量和阈值变量 int newCap, newThr = 0; if (oldCap > 0) { // 如果旧哈希表容量大于 0(已初始化)
// 如果旧哈希表容量达到最大限制(2^30),则不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果旧哈希表容量的两倍小于最大限制且旧容量大于默认容量(16)
// 则将新哈希表的容量设置为旧容量的两倍,新阈值设置为旧阈值的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1; // 阈值加倍
}
} else if (oldThr > 0) {
// 如果旧哈希表未初始化,但旧阈值大于 0,将新哈希表容量设置为旧阈值
// 这种情况发生在:HashMap 使用带容量参数的构造函数创建时
newCap = oldThr;
} else {
// 初始化情况:旧哈希表未初始化且旧阈值小于等于0
// 设置新哈希表的容量和阈值为默认值
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);
}
// 更新 HashMap 的阈值
threshold = newThr;
// 创建一个新的 Node 数组(新哈希表),大小为新容量
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将旧哈希表赋值给新哈希表
table = newTab;
// 如果旧哈希表非空,将旧哈希表中的元素移到新哈希表中
if (oldTab != null) {
// 遍历旧哈希表的每个桶(Node)
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果当前桶非空,将当前桶(链表)中的元素移到新哈希表中
if ((e = oldTab[j]) != null) {
// 将旧哈希表中的当前节点置空,以便 GC 回收
oldTab[j] = null;
// 如果当前节点没有下一个节点(链表长度为1)
if (e.next == null) {
// 直接将当前节点放入新哈希表中
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) {
// 如果当前节点是红黑树节点,调用红黑树的分割方法进行处理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
} else {
// 如果当前节点是链表节点,遍历并将链表中的元素重新映射到新哈希表中
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
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 {
// 如果元素在新哈希表中的位置为原来的位置 + oldCap
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;
}
问:e.hash & oldCap == 0 为什么不是和oldCap-1位与? 这里我们需要考虑oldCap的特性以及rehash过程中元素索引位置的变化。oldCap通常为2的幂(如2、4、8、16等),因此它的二进制形式的最高位是1,其他位都是0。如 oldCap = 8时,二进制表示为1000`。
在 rehash 过程中,新的哈希表容量为newCap = 2 * oldCap,即新容量是旧容量的2倍。此时,元素索引的计算方式发生变化,但由于哈希值保持不变,因此只需考虑容量的影响。
假设元素原本在旧哈希表中的索引为index,则在新哈希表中的索引为 index 或 index + oldCap。为了判断元素位置是否发生变化,我们可以通过将元素哈希值e.hash与旧容量oldCap进行按位与运算来检查。
- 如果
e.hash & oldCap == 0,则说明元素哈希值的二进制表示中与oldCap最高位1对应的位置为0。这意味着元素在扩容后的哈希表中的索引与原来相同,即index。 - 如果
e.hash & oldCap != 0,则说明元素哈希值的二进制表示中与oldCap最高位1对应的位置为1。这意味着元素在扩容后的哈希表中的索引为index + oldCap。
综上所述,我们可以使用 e.hash & oldCap 进行判断,而无需使用 e.hash & (oldCap - 1)。
问:可以使用除法实现这个功能吗?
index = e.hash % newCap;
位运算方法更加高效,因为与、或和非等位操作可以在硬件层面直接实现,而除法运算相对较慢。此外,位运算方法只能在哈希表容量为 2 的幂时使用,这是因为 (newCap - 1) 的二进制表示中所有位都是 1。这种情况下,位运算和除法运算得到的结果是相同的。
所以,在哈希表实现中,通常选择将容量设置为 2 的幂,以便使用位运算进行快速计算。如果使用除法实现这个功能,虽然可以得到正确的结果,但计算效率会降低。