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