一、初始化阶段(确定新容量和阈值)
java
final Node<K,V>[] resize() {
// 1. 保存旧的哈希表
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量
int oldThr = threshold; // 旧阈值
int newCap, newThr = 0; // 新容量,新阈值(初始为0)
二、计算新容量和新阈值(三种情况)
情况1:正常扩容(已有数据)
java
if (oldCap > 0) { // 哈希表已初始化
if (oldCap >= MAXIMUM_CAPACITY) { // 已达最大容量(2^30)
threshold = Integer.MAX_VALUE; // 阈值设为最大整数
return oldTab; // 不再扩容,直接返回
}
// 容量翻倍(左移1位),且未超过最大容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值也翻倍
}
情况2:首次初始化(通过构造器指定了容量)
java
else if (oldThr > 0) // 阈值已设置(构造器指定了初始容量)
newCap = oldThr; // 新容量 = 旧阈值
情况3:首次初始化(使用默认值)
java
else { // 完全默认初始化
newCap = DEFAULT_INITIAL_CAPACITY; // 默认容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认阈值12
}
补充计算新阈值
java
if (newThr == 0) { // 前两种情况可能需要重新计算阈值
float ft = (float)newCap * loadFactor; // 容量 × 负载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // 更新阈值
三、创建新哈希表并迁移数据
java
// 1. 创建新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 2. 迁移数据(如果旧表不为空)
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { // 遍历每个桶
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 桶不为空
oldTab[j] = null; // 清空旧桶,帮助GC
// 情况A:单个节点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e; // 直接放入新桶
// 情况B:红黑树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 情况C:链表节点(重点!保持顺序的优化)
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; // 原位置+oldCap
}
}
}
}
}
return newTab;
四、核心扩容机制解析
1. 扩容时机
- 当元素数量 > 容量 × 负载因子(默认0.75)时触发
- 例如:默认容量16,阈值12,插入第13个元素时扩容
2. 扩容大小
- 新容量 = 旧容量 × 2(除非已达最大容量)
- 新阈值 = 旧阈值 × 2
3. 数据迁移优化(核心亮点)
传统方式 :对每个节点重新计算 hash & (newCap-1)
Java 8优化方式 :利用位运算 (e.hash & oldCap)
- 因为容量总是2的幂,所以扩容相当于在二进制高位加1
(e.hash & oldCap) == 0:节点留在原位置(低位链表)(e.hash & oldCap) != 0:节点移到原索引+oldCap位置(高位链表)
示例:
- 旧容量:16(10000二进制)
- 新容量:32(100000二进制)
- 假设hash=01010,原索引=01010 & 01111 = 01010 = 10
- 判断:(01010 & 10000) = 0 → 留在位置10
- 假设hash=11010,原索引=11010 & 01111 = 01010 = 10
- 判断:(11010 & 10000) = 10000 ≠ 0 → 移到位置10+16=26
4. 保持顺序
- Java 8在链表迁移时保持了原有顺序
- 避免了Java 7中可能出现的死循环问题
- 同时提高了局部性,对缓存更友好
5. 多线程问题
- HashMap本身不是线程安全的
- 多线程环境下扩容可能导致数据丢失、链表成环等问题
- 建议使用ConcurrentHashMap或Collections.synchronizedMap
五、总结特点
- 倍增扩容:容量总是2的幂,扩容时翻倍
- 高效迁移:利用位运算代替模运算,避免重新hash
- 保持顺序:链表节点保持原有顺序
- 分类处理:对单个节点、链表、红黑树分别处理
- 延迟初始化:首次put时才真正创建数组
这种设计在保证性能的同时,减少了哈希冲突,提高了查询效率。