HashMap 面试全攻略(Java)
面试问 HashMap,本质就是在问:你到底懂不懂数据结构 + 并发 + JDK 演进 。
这份文档就是冲着"高级 / 资深 Java 面试"来的。
1. HashMap 是什么?
- 基于 数组 + 链表 + 红黑树 的 Key-Value 数据结构
- 非线程安全
- 允许
null key和null value - JDK 1.8 之后:链表长度 ≥ 8 且数组长度 ≥ 64 → 链表转红黑树
2. HashMap 的核心数据结构
text
Node<K,V>[] table
JDK 1.7
数组 + 链表(头插法)
JDK 1.8
数组 + 链表 + 红黑树(尾插法)
3. put() 底层流程(高频)
- 判断 table 是否为空 → resize
- 计算 hash
- (n - 1) & hash 定位桶位
- 桶为空 → 直接插入
- 桶不为空:
- key 相同 → 覆盖
- 链表 → 尾插
- 判断是否树化
- 判断是否需要扩容
👉 面试金句 :
HashMap 的性能核心在于 hash 的均匀性 + 扩容成本控制
4. hash 为什么要高 16 位异或?
java
hash = h ^ (h >>> 16)
原因
- HashMap 的数组长度较小
- 直接取低位容易冲突
- 高低位混合,减少 hash 冲突
5. resize 扩容机制(必问)
- 默认容量:16
- 扩容因子:0.75
- 扩容后容量:2 倍
JDK 1.8 的优化点
- 不再重新 hash
- 利用
(hash & oldCap)判断新位置
text
0 → 原位置
1 → 原位置 + oldCap
👉 扩容时间复杂度:O(n)
6. 为什么负载因子是 0.75?
权衡三点:
- 空间利用率
- 冲突概率
- resize 成本
0.75 是 经验最优解,不是拍脑袋
7. 链表转红黑树的条件
同时满足:
- 链表长度 ≥ 8
- table.length ≥ 64
否则:优先扩容而不是树化
👉 这是为了避免小数组下树化带来的额外性能损耗
8. 为什么 JDK 1.7 用头插法,1.8 改成尾插?
JDK 1.7 问题
- 扩容时可能 链表成环
- 多线程 put → 死循环 100% CPU
JDK 1.8 改进
- 尾插法
- 扩容时顺序不变
- 解决死循环问题(但依然不线程安全)
9. HashMap 为什么线程不安全?
典型问题
- 数据覆盖
- resize 并发导致数据丢失
- size 不准
- JDK 1.7 死循环
👉 面试官想听的是:结构性原因,不是"没加锁"
10. HashMap vs ConcurrentHashMap
| 对比点 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全 | ❌ | ✅ |
| 锁粒度 | 无 | CAS + synchronized |
| 性能 | 高 | 略低 |
| null key | 支持 | 不支持 |
11. equals & hashCode 的关系(送分题)
规则:
- equals 相等 → hashCode 必须相等
- hashCode 相等 → equals 不一定相等
👉 写自定义 key 必须 重写 hashCode + equals
12. 常见面试连环追问
Q1:HashMap 查找复杂度?
- 理想:O(1)
- 最坏(链表):O(n)
- 红黑树:O(log n)
Q2:为什么容量必须是 2 的幂?
(n - 1) & hash等价于% n- 位运算更快
- 分布更均匀
Q3:Hash 冲突怎么解决?
- 拉链法
- 树化
13. 高频面试总结(直接背)
- HashMap 本质是 空间换时间
- JDK 1.8 的核心升级是:
- 红黑树
- 尾插法
- 高效 resize
- 面试重点永远在:
- put / get
- resize
- 并发问题
14. 一句话总结(终极)
HashMap 的精髓不是 API,而是 在冲突、扩容、并发之间做工程权衡。