说说 HashMap 的扩容机制?如何扩容的?
说说 HashMap 的扩容机制?如何扩容的?
zhangchangsheng面试考察点
- 基础掌握度:面试官不仅仅是想知道 “会扩容” 这个事实,更是想知道你是否理解扩容的触发条件、扩容阈值计算、以及扩容时元素如何重新分布。
- 源码理解深度:考察你是否读过
resize()方法的源码,是否理解 JDK 1.8 中 “高低位链表” 的优化设计,以及为什么扩容后元素要么在原位置,要么在 “原位置 + 原容量” 的位置。 - 版本演进意识:JDK 1.7 和 JDK 1.8 的扩容机制有显著差异(头插法 vs 尾插法),考察你是否了解这些变化以及背后的原因(死循环问题)。
核心答案
HashMap 的扩容机制可以概括为:当元素数量超过 容量 × 负载因子 时,容量翻倍,并重新分配所有元素。 **** **** **** **** **** ****
| 关键点 | 说明 |
|---|---|
| 默认初始容量 | 16 |
| 默认负载因子 | 0.75 |
| 扩容阈值 | capacity × loadFactor(首次为 12 = 16 × 0.75) |
| 扩容方式 | 容量翻倍(newCap = oldCap << 1) |
| 触发条件 | size > threshold 且当前桶位置非空 |
| 元素重分布 | 根据新增的最高位判断:原位置 或 原位置 + 原容量 |
深度解析
一、扩容触发时机
扩容发生在调用 putVal() 方法插入元素后,满足以下两个条件时触发:
flowchart TB
C1["条件一<br/>size > threshold<br/>(元素数量超过阈值)"]
C2["条件二<br/>tab[i] != null<br/>(当前桶位不为空,发生哈希冲突)"]
C1 -- AND --> R["if (++size > threshold && tab[i] != null)<br/>resize()"]
C2 -- AND --> R
上图展示了扩容的两个必要条件。这里有几个关键点需要理解:
- 为什么需要两个条件? 这是一个性能优化。如果当前桶为空,直接放入即可,不需要立即扩容。只有当发生哈希冲突时,才考虑扩容以减少冲突概率。
- threshold 的计算:
threshold = capacity × loadFactor。默认情况下,首次扩容阈值为 12(16 × 0.75)。 - 负载因子为什么是 0.75? 这是时间和空间的权衡:
- 太大(如 1.0):空间利用率高,但哈希冲突多,查询效率低
- 太小(如 0.5):哈希冲突少,查询效率高,但空间浪费
- 0.75 是经过数学计算得出的平衡值,来自泊松分布
二、扩容核心流程
flowchart TB
S1["1. 计算新容量<br/>newCap = oldCap 左移一位(翻倍)"] --> S2["2. 计算新阈值<br/>newThr = newCap × loadFactor"]
S2 --> S3["3. 创建新数组<br/>newTab = new Node[newCap]"]
S3 --> S4["4. 遍历旧数组,迁移元素<br/>遍历每个桶的链表/红黑树<br/>根据新高位决定新位置"]
S4 --> S5["5. 更新引用<br/>table = newTab"]
上图展示了 HashMap 扩容的完整流程。整个过程分为 5 个关键步骤:
- 步骤一 - 计算新容量:使用位运算
oldCap << 1实现翻倍,效率极高。容量始终保持 2 的幂次方,这是为了让(n - 1) & hash能够均匀分布。 - 步骤二 - 计算新阈值:新阈值也需要翻倍,保持负载因子的约束。
- 步骤三 - 创建新数组:在堆中分配一个新的
Node数组,大小为新容量。 - 步骤四 - 迁移元素:这是最核心的步骤,需要遍历旧数组的每个桶,将链表或红黑树中的元素重新分配到新数组中。
- 步骤五 - 更新引用:将
table指向新数组,完成扩容。
三、元素重分布原理(核心!)
这是面试中最重要的考点。JDK 1.8 中,扩容后元素的位置分布有一个巧妙的规律:
flowchart TB
A["扩容前(容量 16,n = 10000)<br/>索引 = (n-1) & hash = 01111 & hash<br/>只看低 4 位"] --> B["扩容后(容量 32,n = 100000)<br/>索引 = (n-1) & hash = 11111 & hash<br/>看低 5 位"]
B --> K["关键:新增的第 5 位决定元素位置<br/>hash 第 5 位 = 0 → 位置不变(还在原索引)<br/>hash 第 5 位 = 1 → 位置 = 原索引 + 16(原容量)"]
K --> EX["示例:元素A 第 5 位 = 0 → 索引 5<br/>元素B 第 5 位 = 1 → 索引 5 + 16 = 21"]
上图揭示了 JDK 1.8 扩容的核心优化。让我详细解释这个精妙的设计:
- 为什么是两种可能? 因为容量从 2^n 变成 2^(n+1),掩码只多了一位。这一位要么是 0,要么是 1,所以元素的新位置只有两种可能。
- 低位链表(loHead/loTail):第 n 位为 0 的元素,扩容后索引不变,组成 “低位链表”。
- 高位链表(hiHead/hiTail):第 n 位为 1 的元素,扩容后索引 = 原索引 + 原容量,组成 “高位链表”。
- 为什么用
(e.hash & oldCap) == 0判断? 这里不是用n-1,而是直接用oldCap(如 16 = 10000)。只有 hash 的第 5 位为 0 时,hash & 16才等于 0。
四、源码核心片段
// JDK 1.8 resize() 方法核心逻辑 |
五、JDK 1.7 vs JDK 1.8 扩容对比
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 链表插入方式 | 头插法 | 尾插法 |
| 扩容后顺序 | 链表元素顺序反转 | 链表元素顺序保持不变 |
| 元素位置计算 | 重新计算 h & (length-1) | 高低位链表优化,无需重新计算 |
| 并发问题 | 多线程扩容可能导致死循环 | 死循环问题已解决 |
| 红黑树 | 无 | 链表 ≥ 8 且容量 ≥ 64 时转红黑树 |
JDK 1.7 死循环问题原因:
flowchart TB
S0["扩容前状态:A → B → null"] --> T1["线程 1 执行到一半暂停<br/>记录了 e = A<br/>记录了 next = B"]
T1 --> T2["线程 2 完成扩容(头插法导致顺序反转)<br/>新表:B → A → null"]
T2 --> T3["线程 1 恢复执行<br/>① 将 A 插入新表头部:A → B(B 的 next 已被线程 2 改为指向 A)<br/>② 处理 B 时,B.next = A,又将 A 插入头部<br/>③ 结果:A 和 B 互相指向,形成环形链表"]
T3 --> W["下次 get() 操作陷入死循环,CPU 飙到 100%"]
上图解释了 JDK 1.7 的经典死循环问题。关键点:
- 头插法:新元素总是插到链表头部
- 并发场景:两个线程同时扩容,一个暂停,另一个完成
- 顺序反转:线程 2 完成后,链表顺序从 A→B 变成 B→A
- 形成环:线程 1 继续执行时,基于旧的 next 指针操作,导致 A 和 B 互指
- JDK 1.8 解决:改用尾插法,顺序保持不变,避免了这个问题
面试高频追问
- 为什么容量必须是 2 的幂次方?
- 让
(n - 1) & hash等价于hash % n,位运算效率更高 - 保证散列均匀,避免空间浪费
- 让
- 扩容时每个元素都需要重新计算 hash 吗?
- 不需要!只需判断新增的高位是 0 还是 1,用
(e.hash & oldCap) == 0即可
- 不需要!只需判断新增的高位是 0 还是 1,用
- HashMap 是线程安全的吗?扩容时会有什么问题?
- 不是线程安全的。JDK 1.7 并发扩容可能死循环,JDK 1.8 可能数据丢失
- 推荐使用
ConcurrentHashMap
- 负载因子可以修改吗?什么场景需要调整?
- 可以,构造函数可指定
loadFactor - 内存紧张时可调高(如 0.85),查询频繁时可调低(如 0.5)
- 可以,构造函数可指定
常见面试变体
- “HashMap 的负载因子为什么是 0.75?”
- “JDK 1.7 和 1.8 的 HashMap 扩容有什么区别?”
- “为什么 JDK 1.8 改用尾插法?解决了什么问题?”
- “扩容时元素如何重新分配?为什么只有两种可能?”
记忆口诀
扩容三要素:容量翻倍、阈值翻倍、高低位分家 位置判断:新位为 0 留原地,新位为 1 加原容((hash & oldCap) == 0 → 低位链表,否则高位链表)
总结
HashMap 扩容在 size > threshold 时触发,容量翻倍(<< 1),使用高低位链表优化元素重分布——根据 hash 新增位的值,元素要么留在原位置,要么移动到 “原位置 + 原容量”。JDK 1.8 改用尾插法解决了 JDK 1.7 并发扩容的死循环问题。记住:负载因子 0.75、容量 2 的幂、高低位分家。
声明:本文转载自小哈的博客 《说说 HashMap 的扩容机制?如何扩容的?》,版权归原文作者所有。转载请保留本声明与原文链接。
评论
匿名评论隐私政策
WalineTwikoo
