这是 HashMap 性能优化的核心,也是面试中区分"背题"和"真懂"的分水岭。1.8 在扩容时做了一个非常巧妙的优化:不需要重新计算哈希值,而是通过位运算直接判断节点去新数组的哪个位置。
- 核心优化:为什么 1.8 扩容更快?
在 1.7 中,扩容时需要遍历所有节点,对每个 key 重新计算 hash (hash(key) % newCapacity),这非常耗时。
在 1.8 中,因为容量始终是 2 的幂次方,利用了一个数学特性:
元素在扩容后的新位置,要么在「原下标」,要么在「原下标 + 旧容量」。
原理推导
假设旧容量 oldCap = 16 (二进制 10000),新容量 newCap = 32 (二进制 100000)。
某个 key 的 hash 值为 H。
旧下标:H & (16 - 1) 即 H & 15 (...01111)
新下标:H & (32 - 1) 即 H & 31 (...11111)
区别仅在于第 5 位(从右往左数,对应 16 的那一位)是 0 还是 1:
如果 H & oldCap == 0:说明第 5 位是 0,新下标 = 旧下标。
如果 H & oldCap != 0:说明第 5 位是 1,新下标 = 旧下标 + oldCap。
结论:只需要做一次 hash & oldCap 的位运算,就能决定节点去向,无需重新计算整个 hash。
- 扩容流程图解(高位/低位链表拆分)
1.8 在扩容时,会将原链表拆分成两个新链表:
低位链表 (loHead/loTail):去往「原下标」
高位链表 (hiHead/hiTail):去往「原下标 + 旧容量」
示例
假设 oldCap = 16,桶位 index=2 处有链表:A -> B -> C
判断 A:hash(A) & 16 == 0 → 放入低位链表
判断 B:hash(B) & 16 != 0 → 放入高位链表
判断 C:hash© & 16 == 0 → 放入低位链表
结果:
低位链表 (index=2):A -> C
高位链表 (index=2+16=18):B
注意:这里使用的是尾插法逻辑(维护 loTail/hiTail),所以顺序保持为 A->C 和 B,不会反转,从而避免了 1.7 的死循环问题。
- 核心代码逻辑(简化版)
java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int newCap = oldCap << 1; // 容量翻倍
// ...省略阈值判断...
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 帮助GC
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;
// 关键判断:hash & oldCap
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;
}
- 为什么这个优化很重要?
1.性能提升:避免了大量的 hash() 计算和取模运算,扩容速度显著提升。
2.数据分布均匀:由于 hash 值的随机性,大约一半的数据留在原位,一半的数据迁移到 index + oldCap,保证了新数组负载因子的平衡。
3.线程安全改进:虽然 HashMap 依然不是线程安全的,但这种保持顺序的尾插拆分彻底解决了 1.7 头插法导致的环形链表死循环问题。 - 面试高频追问
Q: 为什么 HashMap 容量必须是 2 的幂次方?
A:
1.为了高效取模:hash % length 等价于 hash & (length - 1),位运算比取模快得多。
2.为了扩容优化:只有容量是 2 的幂,扩容时才满足「新下标 = 原下标 或 原下标+旧容量」的特性,才能实现上述的高效拆分。
Q: 如果初始容量传入 10,HashMap 怎么处理?
A: 它会通过 tableSizeFor(initialCapacity) 方法,找到大于等于 10 的最小的 2 的幂,即 16。