面试现场:HashMap 的灵魂拷问
面试官:你好,谢飞机,请坐。我看你简历上写了熟悉 Java 基础,那我们先从最基础的开始——HashMap 的底层实现原理是什么?
谢飞机(自信一笑):这个我背过!HashMap 是基于哈希表实现的 Map 接口,它允许 null 键和 null 值,不保证有序,而且是线程不安全的。
面试官:嗯,继续说,具体怎么存的?
谢飞机:JDK 8 之前是数组 + 链表,发生哈希冲突的时候就拉链。JDK 8 之后,当链表长度超过 8 并且数组长度大于 64,就会转成红黑树,防止查找退化成 O(n)。
面试官(点头):不错,那 hash 值是怎么计算的?为什么不是直接用 key.hashCode()?
谢飞机:因为 hashCode() 可能很大,直接取模会分布不均。所以 HashMap 会对 hashCode 进行一次扰动处理,也就是 h ^ (h >>> 16),让高位也参与运算,减少碰撞。
面试官:很好。那初始化容量是 16,负载因子是 0.75,能说说扩容机制吗?
谢飞机:每次 put 的时候会检查 size 是否超过 threshold(容量 * 负载因子),超过就扩容为原来的 2 倍。然后重新计算每个元素的位置,JDK 8 优化了迁移过程,不用重新 hash,通过 (e.hash & oldCap) == 0 判断是在原位置还是原位置 + oldCap。
面试官:回答得非常清晰,基本功扎实。那我们再深入一点——如果多个线程同时 put,并且触发扩容,可能会出现什么问题?
谢飞机(挠头):呃……会出现……数据丢失?或者 CPU 100%?我记得好像是链表成环……
面试官:对,在 JDK 7 中多线程扩容确实可能形成环形链表,导致 get 时死循环。JDK 8 虽然不会成环,但依然线程不安全,比如 A 线程 put 完还没发布,B 线程 get 就看不到。
谢飞机:那……要线程安全的话,可以用 Hashtable 或者 ConcurrentHashMap!
面试官:没错。今天的面试就到这里,整体表现不错,基础知识掌握得可以。你先回去等通知吧。
答案详解:HashMap 底层原理全解析
1. 数据结构
- JDK 8 之前:
Entry数组 + 单向链表 - JDK 8 开始:
Node数组 + 单向链表 + 红黑树(链表长度 ≥ 8 且数组长度 ≥ 64)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
2. hash 扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 高位参与运算,降低碰撞概率
3. 初始化与扩容
- 默认初始容量:16
- 负载因子:0.75(平衡空间与时间)
- 扩容阈值:capacity * loadFactor = 12
- 扩容为原容量的 2 倍
- 扩容优化:JDK 8 中,元素要么在原位置,要么在原位置 + oldCap,无需重新 hash
4. 链表转红黑树条件
- 链表长度 ≥ 8
- 数组长度 ≥ 64
- 否则优先选择扩容
5. 线程安全问题
- 非线程安全:多线程 put 可能导致数据覆盖、死循环(JDK 7 成环)、CPU 100%
- 替代方案:
Hashtable:方法加 synchronized,性能差Collections.synchronizedMap():包装,仍需外部同步ConcurrentHashMap:分段锁 / CAS + synchronized,推荐使用
6. 实际应用建议
- 预估数据量,避免频繁扩容
- 自定义类型作 key 时,必须重写
hashCode()和equals() - 高并发场景务必使用
ConcurrentHashMap
本文通过“谢飞机”这一角色,以轻松方式讲解技术难点,帮助 Java 求职者在笑声中掌握核心知识。