返回首页面试题

面试必备:HashMap 原理深度解析

2026年03月25日6 min read

面试必备:HashMap 原理深度解析

前言

HashMap 是 Java 面试中的高频考点,几乎每个面试官都会问到。这篇文章用图解 + 核心代码,带你彻底搞懂 HashMap。

数据结构演进

JDK 1.7 之前:数组 + 链表

[table数组]
    │
    ├── 桶0 → [Node] → [Node] → null     (链表解决哈希冲突)
    ├── 桶1 → null
    ├── 桶2 → [Node]                     (单个节点)
    └── ...

问题:链表过长时,查询时间复杂度退化为 O(n)

JDK 1.8 开始:数组 + 链表 + 红黑树

[table数组]
    │
    ├── 桶0 → [Node] → [Node] → [Node]  (链表长度<8,仍用链表)
    ├── 桶1 → [TreeNode] → [TreeNode]   (链表长度≥8,转红黑树)
    └── ...

优化:当链表长度超过 8 时,自动转为红黑树,查询时间复杂度从 O(n) → O(log n)

核心原理

1. 哈希计算

// HashMap 的 hash 方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 计算数组下标
int index = (n - 1) & hash

为什么用 h ^ (h >>> 16)

  • 把 hashCode 的高 16 位和低 16 位进行异或
  • 让高位也参与运算,减少哈希冲突

2. put 流程图

put(key, value)
    │
    ▼
① 计算 hash 值
    │
    ▼
② 计算数组下标 (n-1) & hash
    │
    ▼
③ 桶为空?──是──→ 直接插入
    │
    否
    │
    ▼
④ 桶中已有节点
    │
    ├── key 相同?──是──→ 覆盖 value
    │
    否
    │
    ├── 是红黑树节点?──是──→ 红黑树插入
    │
    否
    │
    └── 链表插入
        │
        ├── 链表长度 ≥ 8?──是──→ 转为红黑树
        │
        └── 插入尾部

3. 扩容机制

// 扩容条件:元素数量 > 阈值
// 阈值 = 容量 × 负载因子(默认 0.75)

// 扩容时容量翻倍
newCap = oldCap << 1  // 16 → 32 → 64 → ...

// 所有元素重新哈希(这是个性能损耗点!)

扩容时机:元素数量达到 容量 × 0.75 时触发

扩容过程:新建数组,重新计算每个元素的位置(全部 rehash)

4. 初始容量选择

// 如果预估有 1000 个元素,推荐这样创建
// 1000 / 0.75 + 1 = 1334,再找最近的 2 的幂:2048
Map<Integer, String> map = new HashMap<>(2048);

为什么建议指定初始容量?

  • 避免频繁扩容
  • 每次扩容都需要 rehash,影响性能

线程安全问题

HashMap 不是线程安全的!

并发场景下的问题

  1. 数据覆盖:两个线程同时 put,可能覆盖对方的数据
  2. 死循环:JDK 1.7 扩容时,头插法可能导致环形链表,get 时死循环
  3. ConcurrentModificationException:迭代时修改结构

解决方案

方案特点
Hashtablesynchronized,效率低,已淘汰
Collections.synchronizedMap()包装后加锁,同步方法
ConcurrentHashMap推荐,分段锁 / CAS,效率高
// 推荐写法
Map<String, Object> map = new ConcurrentHashMap<>();

JDK 1.7 vs 1.8 区别

特性JDK 1.7JDK 1.8
数据结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
扩容重新哈希重新哈希
哈希算法9次扰动2次扰动

面试高频问题

Q1:为什么链表转红黑树的阈值是 8?

统计学结论:哈希值均匀分布时,单个桶中节点数超过 8 的概率不到千万分之一。

// 源码注释
/**
 * Because TreeNodes are about twice the size of regular nodes,
 * we only use them when bins contain enough nodes to warrant
 * use (TREEIFY_THRESHOLD = 8).
 */

Q2:负载因子为什么是 0.75?

时间和空间的权衡:

  • 太小(如 0.5):扩容频繁,空间浪费
  • 太大(如 1.0):哈希冲突增多,查询变慢
  • 0.75 是经验和统计得出的平衡点

Q3:HashMap 的容量为什么是 2 的幂?

index = (n - 1) & hash
  • n - 111111... 形式的二进制
  • 与 hash 进行 & 运算,相当于取模,但效率更高

Q4:为什么重写 equals 必须重写 hashCode?

// 两个对象 equals 相等,hashCode 必须相等
Student s1 = new Student("张三", 18);
Student s2 = new Student("张三", 18);

s1.equals(s2)  // true
s1.hashCode() == s2.hashCode()  // 必须 true,否则 HashMap 查询会出问题

总结

  • 数据结构:数组 + 链表(JDK 1.8 加了红黑树)
  • 核心:哈希计算、扩容机制、并发问题
  • 高频考点:put 流程、扩容时机、线程安全、1.7 vs 1.8 区别

评论区