一文搞懂HashMap底层原理
HashMap 的底层原理,这是 Java 集合框架中最核心、最常用的知识点,理解它能帮你解决实际开发中的哈希冲突、扩容、线程安全等问题。我会从数据结构、核心流程、扩容机制、JDK 版本差异四个维度,由浅入深拆解 HashMap 的底层逻辑。
一、HashMap 核心定位与基础结构
1. 核心定位
HashMap 是基于哈希表实现的 Map 接口实现类,用于存储 key-value 键值对,核心特点:
- 允许
key/value为null(key仅允许一个null,value可多个); - 无序(不保证插入顺序和遍历顺序一致);
- 非线程安全(多线程操作可能导致死循环、数据丢失,线程安全场景用 ConcurrentHashMap);
- JDK 1.8 后引入“红黑树”优化,解决哈希冲突导致的链表过长问题。
2. 底层数据结构(JDK 1.7 vs 1.8)
| JDK 版本 | 核心结构 | 解决哈希冲突方式 |
|---|---|---|
| 1.7 | 数组 + 单向链表 | 链表头插法 |
| 1.8 | 数组(Node 数组,称为“桶”) + 单向链表 + 红黑树 | 链表尾插法;链表长度≥8 且数组长度≥64 时转红黑树;红黑树节点数≤6 转回链表 |
核心结构拆解:
- 数组(桶):默认初始容量 16(2^4),下标由
key的hashCode()经过“扰动函数”计算得到,目的是均匀分布元素; - 链表:解决“哈希冲突”(不同
key计算出相同数组下标); - 红黑树:当链表过长(≥8)时,将链表转为红黑树,将查找时间复杂度从 O(n) 优化为 O(logn)。
二、HashMap 核心核心计算逻辑
1. 哈希值计算(扰动函数)
HashMap 不直接使用 key.hashCode() 的返回值,而是通过“扰动函数”重新计算哈希值,目的是减少哈希冲突,让哈希值的高位也参与数组下标的计算:
JDK 1.8 扰动函数(简化版):
static final int hash(Object key) {
int h;
// key为null时哈希值为0;否则将hashCode的高16位和低16位异或,混合高位和低位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2. 数组下标计算
通过哈希值计算数组下标,保证结果在数组长度范围内:
// n 是数组长度(必须是2的幂),i 是最终数组下标
int i = (n - 1) & hash;
- 为什么用
(n-1) & hash而非取模?
当n是 2 的幂时,(n-1) & hash等价于hash % n,但位运算比取模运算快得多(JDK 强制数组长度为 2 的幂)。
三、HashMap 核心操作流程(JDK 1.8)
为了让你更清晰、精准地掌握 JDK 1.8 中 HashMap 的核心操作流程,我会拆解插入(put())、查询(get())、扩容(resize()) 三大核心流程,结合流程图和关键细节说明,覆盖所有核心逻辑。
1.插入/更新元素(put() 方法)
这是 HashMap 最核心的操作,包含哈希计算、冲突处理、树化、扩容等关键逻辑。
1. 完整流程图
graph TD
A["调用 put(key, value)"] --> B["调用 hash(key) 计算哈希值(扰动函数)"]
B --> C["计算数组下标 i = (数组长度n - 1) & hash"]
C --> D{桶table[i] 是否为空?}
D -->|是| E["创建 Node 节点,放入 table[i],size+1"]
D -->|否| F{桶中第一个节点的 key 是否等于当前 key(equals)?}
F -->|是| G["替换该节点的 value,流程结束"]
F -->|否| H{桶中节点是红黑树(TreeNode)?}
H -->|是| I["红黑树中插入/更新节点(putTreeVal)"]
H -->|否| J["遍历链表,查找相同 key"]
J --> K{找到相同 key?}
K -->|是| L["替换 value,流程结束"]
K -->|否| M["链表尾插新 Node 节点,size+1"]
M --> N{链表长度 ≥ 8 且数组长度 ≥ 64?}
N -->|是| O["将链表转为红黑树(treeifyBin)"]
N -->|否| P{数组长度 < 64?}
P -->|是| Q["先执行扩容(resize),不转树"]
P -->|否| R["不处理,继续"]
E --> S{size > 阈值(capacity×loadFactor)?}
I --> S
O --> S
Q --> S
R --> S
S -->|是| T["执行扩容(resize)"]
S -->|否| U["流程结束"]
T --> U
style G fill:#90EE90,stroke:#333,stroke-width:2px
style O fill:#FFA500,stroke:#333,stroke-width:2px
style T fill:#FFB6C1,stroke:#333,stroke-width:2px
2. 关键细节说明
- 哈希值计算:
hash(key)把key.hashCode()的高16位和低16位异或,目的是让高位也参与下标计算,减少哈希冲突; - 桶为空直接插入:最理想的情况,无冲突,时间复杂度 O(1);
- key 相等替换 value:HashMap 中
key的相等判断规则是hashCode 相等 + equals 相等; - 链表尾插:JDK 1.8 改用尾插法,解决 1.7 头插法扩容时的链表死循环问题;
- 树化条件:必须同时满足“链表长度≥8 + 数组长度≥64”——若数组长度<64,优先扩容而非转树(避免小数组浪费红黑树空间);
- 扩容触发:插入后
size超过阈值(默认 16×0.75=12),触发扩容,数组长度翻倍。
2.查询元素(get() 方法)
查询流程是插入流程的逆过程,核心是“快速定位桶 → 精准匹配节点”。
1. 完整流程图
graph TD
A["调用 get(key)"] --> B["计算 key 的哈希值(hash(key))"]
B --> C["计算数组下标 i = (n-1) & hash"]
C --> D{桶table[i] 是否为空?}
D -->|是| E["返回 null,流程结束"]
D -->|否| F{桶中第一个节点的 key 与查询 key 相等?}
F -->|是| G["返回该节点的 value"]
F -->|否| H{桶中节点是红黑树?}
H -->|是| I["红黑树中查找(getTreeNode),返回匹配的 value(无则 null)"]
H -->|否| J["遍历链表,逐个比较 key 的 hashCode + equals"]
J --> K{找到匹配节点?}
K -->|是| L["返回对应 value"]
K -->|否| M["返回 null"]
G --> N["流程结束"]
I --> N
L --> N
M --> N
2. 关键细节说明
- 空桶直接返回 null:无哈希冲突,快速判断;
- 红黑树查找优化:红黑树的查找时间复杂度是 O(logn),远优于链表的 O(n),这是 JDK 1.8 最核心的性能优化;
- key 匹配规则:必须同时满足
hashCode 相等 + equals 相等,缺一不可(比如两个对象 hashCode 相同但 equals 不同,仍视为不同 key)。
3.扩容(resize() 方法)
扩容是 HashMap 性能的核心影响因素,JDK 1.8 对扩容逻辑做了大幅优化。
1. 完整流程图
graph TD
A["触发扩容(size>阈值/数组过小)"] --> B["计算新容量和新阈值"]
B --> C["新容量 = 原容量 × 2(保证2的幂);新阈值 = 新容量 × 负载因子"]
C --> D["创建新的 Node 数组(容量为新容量)"]
D --> E["遍历原数组的每个桶"]
E --> F{桶是否为空?}
F -->|是| G["跳过该桶,继续遍历"]
F -->|否| H{桶中是单个节点?}
H -->|是| I["计算新下标(原下标 或 原下标+原容量),放入新数组"]
H -->|否| J{桶中是红黑树?}
J -->|是| K["拆分红黑树为两个子树,分别放入新数组对应下标"]
J -->|否| L["遍历链表,拆分为两个链表(low链/高链)"]
L --> M["low链放入原下标,高链放入原下标+原容量"]
I --> N["继续遍历下一个桶"]
K --> N
M --> N
N --> O{遍历完成?}
O -->|是| P["将新数组赋值给 table,完成扩容"]
O -->|否| E
P --> Q["流程结束"]
2. 关键细节说明
- 新下标计算优化:JDK 1.8 无需重新计算哈希值,只需判断哈希值的“第 k 位”(k 是原容量的二进制位数)——0 则原下标,1 则原下标+原容量,大幅提升扩容效率;
- 链表拆分:将原链表拆分为“low链”(新下标=原下标)和“高链”(新下标=原下标+原容量),无需反转链表(解决 1.7 死循环问题);
- 红黑树拆分:若红黑树节点数拆分后≤6,会转回链表,避免红黑树的维护开销。
3. 负载因子的意义
默认负载因子 0.75 是“时间/空间”的平衡:
- 负载因子过大:数组利用率高,但哈希冲突概率增加,链表/红黑树变长,查询变慢;
- 负载因子过小:哈希冲突少,但数组扩容频繁,空间浪费多。
总结
- put() 核心:哈希计算→定位桶→空桶直接插→冲突则匹配key/链表尾插/红黑树插入→检查树化/扩容条件;
- get() 核心:哈希计算→定位桶→空桶返回null→匹配首节点→红黑树/链表查找;
- resize() 核心:容量翻倍→新下标快速计算→拆分节点(单节点/红黑树/链表)→迁移到新数组,JDK 1.8 优化了下标计算和链表拆分,解决了死循环问题。
这三大流程覆盖了 HashMap 日常使用的所有核心逻辑,理解后能轻松应对哈希冲突、性能调优、线程安全等实际开发问题。
五、JDK 1.7 vs JDK 1.8 核心差异
| 对比维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 数组 + 单向链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法(扩容时链表反转) | 尾插法(扩容时链表不反转) |
| 扩容后下标计算 | 重新计算哈希值 → 取模 | 利用哈希值的某一位直接判断(优化) |
| 死循环风险 | 多线程扩容可能导致链表死循环 | 解决死循环问题(尾插+下标优化) |
| 查询性能 | 链表越长,性能越差(O(n)) | 长链表转红黑树(O(logn)) |
| null key 处理 | 哈希值固定为 0,放入下标 0 | 逻辑一致,但结合红黑树处理 |
六、核心代码示例(简化版)
import java.util.HashMap;
public class HashMapDemo {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
// 插入元素
map.put("Java", 1);
map.put("Python", 2);
map.put("C++", 3);
map.put(null, 0); // key为null,哈希值0,放入下标0
// 查询元素
System.out.println(map.get("Java")); // 输出:1
System.out.println(map.get(null)); // 输出:0
// 遍历元素(无序)
map.forEach((k, v) -> System.out.println(k + ":" + v));
// 扩容触发:默认初始容量16,阈值12,插入13个元素触发扩容
for (int i = 0; i < 13; i++) {
map.put("key" + i, i);
}
System.out.println("扩容后容量:" + getCapacity(map)); // 输出:32
}
// 反射获取HashMap的实际容量(仅用于演示)
private static int getCapacity(HashMap<?, ?> map) {
try {
var field = HashMap.class.getDeclaredField("table");
field.setAccessible(true);
Object[] table = (Object[]) field.get(map);
return table == null ? 0 : table.length;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
输出说明:
- 插入
nullkey 时,哈希值固定为 0,放入数组下标 0; - 插入 13 个元素后,触发扩容,数组容量从 16 变为 32。
七、常见面试/开发高频问题
- 为什么 HashMap 的容量必须是 2 的幂?
答:保证(n-1) & hash等价于取模,且扩容时可通过哈希值的某一位快速判断节点位置,提升性能。 - HashMap 为什么线程不安全?
答:多线程扩容时(1.7)可能导致链表死循环;多线程插入时可能覆盖数据;size 计数非原子操作,导致统计错误。 - 红黑树为什么是 8 转树、6 转回链表?
答:根据泊松分布,链表长度≥8 的概率极低(约 0.00000006),转树性价比高;6 转回是避免频繁在树和链表间切换(设置缓冲区间)。
总结
- HashMap 底层是“数组+链表+红黑树”(JDK 1.8),通过哈希值计算数组下标,链表/红黑树解决哈希冲突;
- 核心优化点:JDK 1.8 尾插法解决死循环、红黑树优化长链表查询、扩容下标计算优化;
- 关键参数:初始容量(16)、负载因子(0.75)、阈值(容量×负载因子),需根据业务场景合理调整(如预知数据量时指定初始容量,减少扩容)。
百流积聚,江河是也;文若化风,可以砾石。

浙公网安备 33010602011771号