Java 中 HashMap 的扩容机制是怎样的?
·
1. 扩容触发条件
当满足以下条件时会触发扩容:
// 当元素数量 size 大于等于阈值 threshold 时
if (++size > threshold)
resize();
其中:
// 阈值 = 容量 × 负载因子
threshold = capacity * loadFactor;
2. 默认参数
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
3. 扩容机制详解
3.1 扩容时机
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 处理哈希冲突
// ...
}
// 检查是否需要扩容
if (++size > threshold)
resize();
return null;
}
3.2 扩容过程
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1. 计算新容量
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量 = 旧容量 × 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新阈值 = 旧阈值 × 2
newThr = oldThr << 1;
}
// ... 其他情况处理
// 2. 创建新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 3. 迁移数据
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
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;
}
// 原索引 + oldCap 位置
else {
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;
}
}
}
}
}
// 4. 更新阈值
threshold = newThr;
return newTab;
}
4. 扩容优化(JDK 1.8)
4.1 索引计算优化
JDK 1.7:
// 每个元素都需要重新计算索引
int index = indexFor(e.hash, newCapacity);
static int indexFor(int h, int length) {
return h & (length - 1);
}
JDK 1.8:
// 通过位运算判断元素位置
// (e.hash & oldCap) == 0 → 位置不变
// (e.hash & oldCap) != 0 → 位置 = 原位置 + oldCap
// 示例:
// oldCap = 16 (10000)
// newCap = 32 (100000)
//
// 元素 hash = 5 (00101)
// 5 & 16 = 0 → 位置不变
//
// 元素 hash = 21 (10101)
// 21 & 16 = 16 → 位置 = 原位置 + 16
4.2 链表迁移优化
// 将链表分成两个链表
// loHead/loTail: 位置不变的节点
// hiHead/hiTail: 位置 = 原位置 + oldCap 的节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
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);
5. 扩容过程图解
5.1 扩容前
容量: 16
阈值: 12 (16 * 0.75)
元素数量: 12
数组结构:
[0] → null
[1] → Node1 → Node2 → Node3
[2] → null
...
[15] → null
5.2 扩容后
容量: 32
阈值: 24 (32 * 0.75)
元素数量: 12
数组结构:
[0] → null
[1] → Node1 → Node3 (hash & 16 == 0)
[2] → null
...
[17] → Node2 (hash & 16 != 0, 位置 = 1 + 16)
...
[31] → null
6. 红黑树处理
6.1 链表转红黑树
// 当链表长度 >= 8 且数组长度 >= 64 时
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, hash);
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
6.2 红黑树转链表
// 当红黑树节点 <= 6 时
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = untreeify(loHead);
static final int UNTREEIFY_THRESHOLD = 6;
6.3 扩容时红黑树处理
// 红黑树节点也会被分成两部分
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> e = this;
// 分成低位和高位两个树
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> p = e; p != null; p = p.next) {
int ph = p.hash;
if ((ph & bit) == 0) {
// 低位树
if ((p.prev = loTail) == null)
loHead = p;
else
loTail.next = p;
loTail = p;
++lc;
} else {
// 高位树
if ((p.prev = hiTail) == null)
hiHead = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 根据节点数量决定是否转回链表
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null)
loHead.treeify(tab);
}
}
// ... 类似处理高位树
}
7. 扩容示例
public class HashMapResizeDemo {
public static void main(String[] args) {
// 创建 HashMap,默认容量 16,负载因子 0.75
HashMap<Integer, String> map = new HashMap<>();
// 添加元素
for (int i = 0; i < 20; i++) {
map.put(i, "Value" + i);
// 打印容量信息
if (i == 11 || i == 12) {
System.out.println("添加第 " + (i + 1) + " 个元素后:");
printCapacityInfo(map);
}
}
}
static void printCapacityInfo(HashMap<?, ?> map) {
try {
// 使用反射获取内部信息
java.lang.reflect.Field tableField =
HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
java.lang.reflect.Field thresholdField =
HashMap.class.getDeclaredField("threshold");
thresholdField.setAccessible(true);
int threshold = (int) thresholdField.get(map);
java.lang.reflect.Field sizeField =
HashMap.class.getDeclaredField("size");
sizeField.setAccessible(true);
int size = (int) sizeField.get(map);
System.out.println(" 容量: " + (table != null ? table.length : 0));
System.out.println(" 阈值: " + threshold);
System.out.println(" 大小: " + size);
System.out.println();
} catch (Exception e) {
e.printStackTrace();
}
}
}
8. 扩容性能分析
8.1 时间复杂度
// 扩容操作
resize() - O(n) // n 是元素数量
// 均摊分析
// 假设扩容从 16 → 32 → 64 → 128
// 每次扩容需要复制所有元素
// 但扩容次数是 log(n)
// 所以均摊到每次 put 操作是 O(1)
8.2 空间复杂度
// 扩容时需要创建新数组
// 临时占用 2 倍内存
// 旧数组会被 GC 回收
9. 扩容优化建议
9.1 预估初始容量
// 如果知道大概的元素数量,设置初始容量
int expectedSize = 1000;
// 计算合适的初始容量:expectedSize / 0.75 + 1
int initialCapacity = (int) (expectedSize / 0.75) + 1;
HashMap<Integer, String> map = new HashMap<>(initialCapacity);
9.2 调整负载因子
// 内存敏感场景,提高负载因子
Map<Integer, String> map = new HashMap<>(16, 0.9f);
// 性能敏感场景,降低负载因子
Map<Integer, String> map = new HashMap<>(16, 0.5f);
9.3 批量操作优化
// 批量添加时,先设置足够大的容量
HashMap<Integer, String> map = new HashMap<>(1000);
for (int i = 0; i < 1000; i++) {
map.put(i, "Value" + i);
}
10. 扩容流程总结
1. 添加元素
↓
2. size++ > threshold?
↓ 是
3. 计算新容量 newCap = oldCap × 2
↓
4. 计算新阈值 newThr = oldThr × 2
↓
5. 创建新数组 newTab[newCap]
↓
6. 遍历旧数组,迁移元素:
- 单个节点:重新计算位置
- 链表:分成两个链表
- 红黑树:分成两个树
↓
7. 更新 table = newTab
↓
8. 更新 threshold = newThr
↓
9. 完成扩容
11. JDK 版本差异
JDK 1.7 vs JDK 1.8
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 扩容方式 | 重新计算所有元素索引 | 优化索引计算 |
| 链表处理 | 重新插入 | 分成两个链表 |
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 头插法 | 是 | 否(尾插法) |
| 并发问题 | 可能死循环 | 不会死循环 |
总结
HashMap 扩容机制的核心要点:
- 触发条件:size > threshold(容量 × 负载因子)
- 扩容倍数:新容量 = 旧容量 × 2
- 索引优化:通过位运算快速确定新位置
- 链表优化:将链表分成两部分,避免重新计算
- 红黑树处理:树节点也会被分成两部分
- 性能:单次 O(n),均摊 O(1)
优化建议:
- 预估初始容量,避免频繁扩容
- 根据场景调整负载因子
- 批量操作前设置合适容量
- 注意 JDK 1.8 的优化特性
更多推荐




所有评论(0)