6.HashMap 从 JDK7 到 JDK21 的演进
HashMap 从 JDK7 到 JDK21 的演进(含红黑树化机制)
🚀 高频指数:★★★★★
🎯 你将收获:底层结构演变、扩容与树化机制、hash冲突解决、并发风险与源码逻辑图。
一、为什么 HashMap 面试永远问不完?
因为它融合了:
- 数组、链表、红黑树三种数据结构;
- 哈希算法、扩容逻辑、并发原理;
- JVM 内存布局与 GC 行为。
☕️ 一句话总结:“一个 HashMap = 一部 Java 底层史。”
二、结构总览:数组 + 链表 + 红黑树
| 组成部分 | 说明 |
|---|---|
| Node<K,V>[] table | 主数组(哈希桶) |
| Node<K,V>.next | 冲突时形成链表 |
| TreeNode<K,V> | 冲突过多时转为红黑树 |
| threshold | 扩容阈值(容量 × 负载因子) |
| loadFactor | 默认0.75,平衡空间与查找效率 |
三、hash 冲突与扰动函数
1️⃣ key → hash
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
🔹 高16位与低16位异或,减少碰撞。
🔹 确保高位参与寻址,分布更均匀。
2️⃣ 取模定位桶
int index = (n - 1) & hash;
✅ 位运算代替
%,提升性能。
例如容量16 → (16-1)=15 → 按位与实现快速取模。
四、JDK7 与 JDK8 的重大差异
| 特性 | JDK7 | JDK8 |
|---|---|---|
| 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法(链表逆序) | 尾插法(链表顺序) |
| 扩容实现 | 重新计算每个元素位置 | 利用 hash 位优化分配 |
| 初始化时机 | 延迟到第一次 put | 同样延迟 |
| 遍历顺序 | 不保证顺序 | 同样不保证 |
| 并发问题 | 存在死循环风险 | 已解决(尾插+安全迁移) |
☕️ 口诀:“七头八尾,八树化。”
五、核心源码解析(JDK8)
1️⃣ put 流程简图
hash → 定位桶 → 无元素则新建Node→ 有元素则:- key相同 → 覆盖value- key不同 → 链表/树追加节点- 链表长度≥8 → 树化- size > threshold → 扩容
2️⃣ treeifyBin 核心逻辑
if (tab == null || (n = tab.length) < 64)resize(); // 数组太小先扩容
elsetab[i] = new TreeNode<>(...); // 链表转红黑树
✅ 树化条件:
- 链表长度 ≥ 8;
- 且数组容量 ≥ 64;
否则优先扩容而非树化。
六、扩容机制详解
扩容阈值计算
threshold = oldCapacity * loadFactor;
resize() 方法逻辑
newCap = oldCap << 1; // ×2 扩容
for (Node<K,V> e : oldTab) {if (e.hash & oldCap == 0)newTab[j] = e; // 留在原桶elsenewTab[j + oldCap] = e; // 移至新桶
}
⚙️ 利用 hash 位的“高1/低0”判断是否迁移 → 无需重新计算 hash。
📌 口诀:“低位留原,高位加偏。”
七、红黑树化机制(JDK8+)
链表节点超过8时树化(TreeNode 结构):
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent, left, right, prev;boolean red;
}
- 插入、删除后会自动旋转、着色;
- 当桶中元素数量 < 6 时退化为链表。
✅ 树化提升最差复杂度从 O(n) → O(log n)。
八、hashCode、equals、冲突与覆盖
put() 时判断 key 相等逻辑:
if (p.hash == hash && (p.key == key || key.equals(p.key))) {p.value = newValue; // 覆盖
}
所以必须保证:equals 与 hashCode 一致性。
九、JDK21 中 HashMap 的变化
🆕 自 JDK19 起,HashMap 内部已针对高性能场景做进一步优化。
| 版本 | 优化内容 |
|---|---|
| JDK19 | 增加随机种子 hash,防御哈希碰撞攻击 |
| JDK20 | 改进 resize 逻辑,减少复制开销 |
| JDK21 | 支持“Compact HashMap”实验性特性(部分场景内存优化) |
| JDK21+ | 新增 computeIfAbsent 等 Lambda 优化内联 |
☕️ 趋势:从“稳定结构”向“高并发与内存友好”方向演进。
十、面试官追问清单
| 面试问题 | 答题要点 |
|---|---|
| HashMap 初始容量? | 默认16,负载因子0.75 |
| 扩容时机? | size > capacity × loadFactor |
| 链表树化条件? | 链表长度≥8且容量≥64 |
| 为什么用红黑树? | 保证查找O(log n),避免退化 |
| JDK7 与 JDK8 最大区别? | 链表头插→尾插 + 树化机制 |
| 并发风险? | JDK7 多线程扩容死循环,JDK8已改进但仍非线程安全 |
十一、项目实践建议
| 场景 | 建议 |
|---|---|
| 数据量已知 | new HashMap<>(expectedSize * 4 / 3) 避免扩容 |
| 多线程环境 | 使用 ConcurrentHashMap |
| Key 为自定义类 | 必须重写 equals 与 hashCode |
| 高性能缓存 | 调整负载因子到0.6–0.7 平衡空间与效率 |
十二、口诀记忆
☕️ “数组为体,链表为骨,红黑为盾,扩容自愈。”
补充版:
“七头八尾八树化,六退回链六十四。”
十三、小结
| 知识点 | 要点 |
|---|---|
| 数据结构 | 数组 + 链表 + 红黑树 |
| 默认参数 | 初始容量16,负载因子0.75 |
| 扩容策略 | ×2扩容,重新分配桶 |
| 树化规则 | 链表≥8且容量≥64 |
| 性能复杂度 | O(1) → O(log n)(树化后) |
| 并发安全 | 非线程安全(推荐ConcurrentHashMap) |
✅ 一句话总结:“HashMap 一半是算法,一半是艺术。”
