在 Java 中,HashMap 是最常用的集合类之一,它的底层实现涉及数组、链表和红黑树。理解它的扩容机制,对于写出高效、安全的代码至关重要。本文将从触发条件、迁移流程、版本差异等方面,全面解析 HashMap 的扩容过程。
一、扩容触发条件
HashMap 的扩容主要发生在两种情况下:
1. 元素个数超过阈值
-
阈值 = 当前数组容量 × 负载因子(默认 0.75)
-
当
size > 数组容量 × 0.75时,触发扩容
2. 链表树化前的容量检查(JDK 1.8)
-
当某个链表的长度超过 8,但数组长度小于 64 时,不会树化,而是先扩容
-
这是为了避免因数组过小而导致频繁的树化操作
二、扩容流程概述
1. 创建新数组
- 新数组容量 = 旧数组容量 × 2
2. 元素迁移
JDK 1.8 对迁移过程做了优化,不再像 1.7 那样逐个重新计算下标,而是通过位运算高效拆分。
关键公式
-
旧下标:
index = hash & (oldCap - 1) -
新下标判断:
-
如果
hash & oldCap == 0,则新下标 = 旧下标 -
如果
hash & oldCap == 1,则新下标 = 旧下标 + 旧容量
-
三、迁移四部曲(JDK 1.8)
第一步:跳过空桶
- 遍历旧数组,如果当前位置为 null,则直接跳过
第二步:单个节点
- 直接计算新下标并放入新数组
第三步:链表拆分
-
根据
hash & oldCap将链表拆分为两条:-
low 链表:新下标 = 旧下标
-
high 链表:新下标 = 旧下标 + 旧容量
-
-
拆分后直接放入新数组,避免逐个插入
第四步:红黑树拆分
-
同样根据
hash & oldCap拆分为两棵树 -
如果某棵树节点数 ≤ 6,则退化为链表
-
否则继续保持红黑树结构
四、JDK 1.7 与 1.8 扩容对比
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 插入方式 | 头插法(扩容后链表倒置) | 尾插法(保持原顺序) |
| 迁移方式 | 重新计算每个下标,逐个插入 | 按高低位拆分,整体迁移 |
| 并发安全 | 扩容时可能出现环形链表 | 避免了环形链表问题 |
| 红黑树支持 | 无 | 支持,扩容时可能退化为链表 |
| 效率 | 较低 | 更高,尤其在大数据量下 |
五、总结
HashMap 的扩容机制在 JDK 1.8 中得到了显著优化,不仅引入了红黑树来提升查找效率,还通过位运算优化了元素迁移过程,避免了链表倒置和环形链表的并发问题。理解这些底层细节,有助于我们在实际开发中更好地使用 HashMap,避免踩坑。