面试官:讲讲 HashMap 的底层实现原理?谢飞机:这题我会!

55 阅读3分钟

面试现场: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 求职者在笑声中掌握核心知识。