一、为什么需要扩容?
HashMap 的底层数据结构是数组 + 链表(JDK 1.8 引入了红黑树)。数组的长度是固定的。随着不断执行 put 操作,越来越多的键值对被放入容器。为了解决哈希冲突,链表会越来越长。
问题:链表越长,查询效率越低,时间复杂度从 O(1) 退化为 O(n)。
解决:当元素数量达到一定阈值时,HashMap 会自动创建一个更大的数组,并将所有数据重新迁移到新数组中。这个过程就是扩容(Resize)。
核心参数:
- Capacity (容量):HashMap 中数组(bucket)的长度,默认 16。
- LoadFactor (负载因子):衡量 HashMap 满的程度,默认 0.75f。
- Threshold (扩容阈值):capacity * loadFactor。当 size 超过这个值时,触发扩容。
为什么负载因子是 0.75? 这是一个空间与时间的折中(Trade-off)。如果是 1.0:空间利用率高,但哈希冲突概率大,查询慢。如果是 0.5:哈希冲突少,查询快,但浪费一半空间。0.75 是基于泊松分布统计学原理得出的黄金平衡点。
二、扩容的触发时机
在 put 方法存入数据后,HashMap 会检查当前元素个数 size 是否大于 threshold。
// 伪代码逻辑
if (++size > threshold) {
resize();
}
JDK 1.8 是先插入数据,再判断是否需要扩容;而 JDK 1.7 是先判断是否需要扩容,再插入数据。
三、扩容流程
当 HashMap 中的元素个数(size)达到负载因子 * 容量之后,会触发扩容。此时会建一个新数组,数组容量为旧数组的两倍:newCap = oldCap * 2。扩容分三种情况:仅有一节点、已形成链表、已形成红黑树。
3.0 高低位拆分原理
JDK 8 利用容量为 2 的幂次方的特性,通过 (e.hash & oldCap) == 0 判断元素在新数组中的位置:
- 低位(lo):hash & oldCap == 0 → 新索引 = 原索引
- 高位(hi):hash & oldCap != 0 → 新索引 = 原索引 + oldCap
这是因为索引计算使用 hash(k) & (cap - 1)。每次扩容都是容量 * 2。假设旧容量为 16,新容量为 32。原本通过 hash(k) & (01111) 计算索引,新数组通过 hash(k) & (011111) 计算。是否迁移取决于第 log(cap) + 1 位。如果 hash & cap == 0 就放置原索引,否则放到原索引 + oldCap。
3.1 仅有一节点
这是最简单的情景,只要把这个节点迁移到新数组即可。
if (e.next == null) {
newTab[e.hash & (newCap - 1)] = e;
}
3.2 已形成链表
迁移步骤:
- 初始化两条空链表
- 低位链表:loHead=null, loTail=null
- 高位链表:hiHead=null, hiTail=null
- 断尾与安置
- loTail.next = null → 低位链表变为 A → C → null
- hiTail.next = null → 高位链表变为 B → D → null
- 低位链表放入 newTab[3]
- 高位链表放入 newTab[19]


