“HashMap 不仅是面试常客,更是 Java 集合的‘心脏’。” 你是否曾好奇:
HashMap 的默认容量是 16?本文将带你深入 JDK 源码,从数据结构、扩容流程、参数设计原理到红黑树转换机制,全面剖析 HashMap 的底层实现。
HashMap 是基于 数组 + 链表 + 红黑树 实现的哈希表:
transient Node<K,V>[] table; // 哈希桶数组Node 节点,初始容量 16⚠️
HashMap不是线程安全的,多线程环境下推荐使用ConcurrentHashMap。
当 HashMap 中的元素个数(size)超过 阈值(threshold) 时,触发扩容。
threshold = capacity * loadFactorcapacity:当前数组容量(如 16、32、64…)loadFactor:加载因子,默认 0.75✅ 触发条件:
size > threshold
if (table == null || table.length == 0)
resize(); // 初始化数组,容量为 16int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量 = 旧容量 << 1(即 ×2)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 新阈值也 ×2
}✅ 扩容后容量翻倍:16 → 32 → 64 → 128…
这是扩容最核心的步骤:将原数组中的每个元素重新分配到新数组中。
在 JDK 1.8 中,由于数组长度始终是 2 的幂,扩容时只需判断:
原索引位置是否需要迁移?
// e.hash & oldCap == 0 ? 原位置 : 原位置 + oldCap(e.hash & oldCap) == 0,元素留在原索引原索引 + oldCap 位置✅ 优势:无需重新计算
hash % n,性能大幅提升!
特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
数据结构 | 数组 + 单向链表 | 数组 + 链表 + 红黑树 |
扩容后 rehash 方式 | 全部重新 hash(indexFor(hash, capacity)) | 利用 hash & oldCap 判断是否迁移 |
链表插入方式 | 头插法(导致环形链表) | 尾插法(避免环) |
并发问题 | 多线程扩容可能形成环,导致死循环 | 仍非线程安全,但避免了环形链表 |
性能 | 扩容慢,rehash 开销大 | 扩容快,rehash 仅位运算 |
JDK 1.7 头插法:
e.next = newTable[bucketIndex];
newTable[bucketIndex] = e;get() 时无限循环,CPU 100%JDK 1.8 尾插法:
loTail.next = e; // 追加到链表尾部✅ JDK 1.8 的设计更安全、更高效。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16hash & (n-1) 快速计算索引(替代取模)✅
index = hash & (capacity - 1)等价于hash % capacity,但位运算更快。
static final float DEFAULT_LOAD_FACTOR = 0.75f;这是时间与空间的权衡:
加载因子 | 空间利用率 | 冲突概率 | 查找性能 |
|---|---|---|---|
过小(如 0.5) | 低 | 低 | 快,但浪费内存 |
过大(如 0.9) | 高 | 高 | 慢,链表变长 |
0.75 | 适中 | 适中 | 平衡 |
🔬 数学依据: 根据泊松分布,当加载因子为 0.75 时,链表长度超过 8 的概率极低(见下文),既能保证空间利用率,又能控制冲突。
static final int TREEIFY_THRESHOLD = 8;这是基于泊松分布(Poisson Distribution) 的统计结果:
JDK 源码注释中明确说明:
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins. In
usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution)
with a parameter of about 0.5 on average for the default resizing
threshold of 0.75...链表长度 | 出现概率(理论值) |
|---|---|
0 | 0.60653066 |
1 | 0.30326533 |
2 | 0.07581633 |
3 | 0.01263606 |
4 | 0.00157952 |
5 | 0.00015795 |
6 | 0.00001316 |
7 | 0.00000094 |
8 | 0.00000006 |
✅ 长度 ≥ 8 的概率仅为 0.00000006,几乎不可能在正常哈希下出现。 一旦达到 8,说明:
此时转换为红黑树(O(log n))是合理选择。
static final int MIN_TREEIFY_CAPACITY = 64;即使链表长度 ≥ 8,也不会立即转红黑树,必须满足:
table.length >= 64
否则,优先选择扩容。
✅ 只有当数组足够大(≥64)且链表仍长(≥8),才说明哈希冲突严重,值得树化。
TREEIFY_THRESHOLD(8)MIN_TREEIFY_CAPACITY(64)if (binCount >= TREEIFY_THRESHOLD - 1) {
if (tab == null || tab.length < MIN_TREEIFY_CAPACITY)
resize(); // 先扩容
else
treeifyBin(tab, i); // 转红黑树
}✅ 保持结构最优,避免“过度树化”。
new HashMap<>(32)HashMap 非线程安全put 可能导致: ✅ 多线程环境使用:
ConcurrentHashMapCollections.synchronizedMap()+-----------------------------+
| 开始 put 操作 |
+-----------------------------+
|
v
+------------------------------+
| 计算 hash 值 & 索引位置 |
| index = hash & (n - 1) |
+------------------------------+
|
v
+------------------------------+
| 当前桶位是否为空? |
+------------------------------+
/ \
是 否
/ \
v v
+-------------+ +------------------------+
| 直接创建新 | | 遍历链表/红黑树 |
| Node 放入 | | 查找 key 是否已存在 |
| 桶位 | +------------------------+
+-------------+ |
|
v
+--------------------------+
| key 是否已存在? |
+--------------------------+
/ \
是 否
/ \
v v
+------------------+ +------------------------+
| 更新旧 value | | 添加新节点到末尾 |
| 返回旧值 | | (尾插法) |
+------------------+ +------------------------+
|
v
+----------------------------------+
| size++ 后是否 > threshold ? |
| (size > capacity * loadFactor) |
+----------------------------------+
|
是
|
v
+-----------------------------+
| 触发 resize() |
+-----------------------------+
|
v
+-----------------------------+
| 当前 table 是否为 null ? |
+-----------------------------+
/ \
是 否
/ \
v v
+---------------------+ +-----------------------------+
| 初始化 table | | oldCap = oldTab.length |
| capacity = 16 | | newCap = oldCap << 1 (×2) |
| threshold = 12 | | newThr = oldThr << 1 |
+---------------------+ +-----------------------------+
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
\ /
v
+----------------------------------+
| 创建新 table 数组 |
| newTab = new Node[newCap] |
+----------------------------------+
|
v
+----------------------------------+
| 遍历 oldTab 中每个桶位 |
+----------------------------------+
|
v
+----------------------------------+
| 当前桶位是否有元素? |
+----------------------------------+
|
否 --(继续下一个桶位)--> [循环]
|
是
|
v
+----------------------------------+
| 元素是单节点?链表?红黑树? |
+----------------------------------+
/ | \
/ | \
/ | \
v v v
+---------------+ +--------------+ +------------------+
| 直接 rehash | | 遍历链表 | | 调用 treeifyBin? |
| 计算新索引 | | 计算新索引 | | 或 split 拆分树 |
| 放入 newTab | | 尾插法迁移 | | 迁移到 newTab |
+---------------+ +--------------+ +------------------+
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\ | /
\|/
v
+----------------------------------+
| 所有元素迁移完成? |
+----------------------------------+
|
否 --(继续循环)--> [上一步]
|
是
|
v
+----------------------------------+
| table = newTab; |
| threshold = newThr; |
| resize 完成,返回新 table |
+----------------------------------+
|
v
+----------------------------------+
| put 操作继续执行 |
+----------------------------------+
|
v
[结束]机制 | 原理 | 数值/条件 |
|---|---|---|
扩容触发 | size > capacity * loadFactor | 默认 0.75 |
扩容方式 | 容量 ×2 | 16 → 32 → 64… |
rehash 优化 | hash & oldCap 判断迁移 | 位运算,高效 |
链表转树 | 长度 ≥8 且 容量 ≥64 | 基于泊松分布 |
树退化 | 节点 ≤6 | 保持结构最优 |
插入方式 | 尾插法(1.8) | 避免环形链表 |
new HashMap<>(expectedSize / 0.75f + 1)hashCode() 和 equals()ConcurrentHashMapHashMap 的设计,是数据结构、概率统计、性能优化的完美结合。
每一个参数(16、0.75、8、64)都不是随意设定,而是经过深思熟虑的工程权衡。
“理解 HashMap,不仅是理解一个集合,更是理解 Java 工程师的思维方式。”
掌握其底层原理,你不仅能轻松应对面试,更能在实际开发中写出高性能、低延迟的代码。