HashMap 底层原理(面试精简版)
1. 底层数据结构
- JDK 1.7:数组 + 链表
- JDK 1.8+:数组 + 链表 + 红黑树
- 链表长度 > 8 且数组长度 ≥ 64 时,链表转为红黑树
- 红黑树节点数 < 6 时,退化为链表
2. 核心常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子
static final int TREEIFY_THRESHOLD = 8; // 树化阈值
static final int UNTREEIFY_THRESHOLD = 6; // 链化阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量
3. Put 流程(JDK 1.8)
- 计算
hash(key):(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)(扰动函数,高低位异或) - 若数组为空,触发
resize()初始化 - 计算索引:
(n - 1) & hash,若该位置为空,直接插入 - 若不为空:
- 检查 key 是否相等,相等则覆盖
- 若为红黑树节点,调用
putTreeVal - 若为链表,遍历到尾插(JDK 1.7 头插,1.8 改为尾插),同时计数,超过 8 则转红黑树
- 插入后判断
size > threshold,是则扩容
4. 扩容机制
- 触发条件:
size >= threshold(threshold = capacity * loadFactor) - 扩容大小:新容量 = 旧容量 << 1(翻倍)
- 重哈希:节点重新计算索引,JDK 1.8 优化:
- 节点在原位置或
原位置 + 旧容量,通过(e.hash & oldCap) == 0判断 - 无需重新计算 hash,减少开销
- 节点在原位置或
5. 线程安全性
- HashMap 非线程安全
- 并发场景下可能产生:
- JDK 1.7 头插法导致死循环(多线程扩容时)
- 数据丢失、size 不准确
- 替代方案:
ConcurrentHashMap、Collections.synchronizedMap
6. 关键知识点
问题
答案
为什么容量是 2 的幂
方便取模运算 (n - 1) & hash,保证均匀分布
负载因子为什么 0.75
时间与空间平衡的折中
为什么引入红黑树
防止链表过长导致查询 O(n) 退化为 O(log n)
null key 存在位置
固定存放在数组 0 号桶
7. JDK 1.7 vs 1.8 区别
对比项
JDK 1.7
JDK 1.8
数据结构
数组 + 链表
数组 + 链表 + 红黑树
插入方式
头插法
尾插法
扩容重哈希
重新计算每个 hash
优化为原位置或原位置+旧容量
扰动函数
多次异或
一次异或(高16位与低16位异或)
面试常问:“HashMap 的 get 流程?” → 计算 hash → 定位桶 → 遍历链表/红黑树,用
equals()比较 key 返回 value。