HashMap 的前世今生

在 Java 后端的世界里,HashMap 是一个绕不开的类。

它既是日常开发中最常用的容器之一,也是面试中被反复追问的“基础题”,甚至还曾因为设计取舍,在并发场景下制造过著名的 CPU 100% 死循环问题。

本文将从 JDK 1.0 到 JDK 24,回顾 HashMap 的设计演进,试图回答三个问题:

  • HashMap 一开始是如何设计的?
  • 为什么 JDK 1.8 对它进行了“重构级”改造?
  • HashMap 背后体现了怎样的工程思想?

为什么 HashMap 如此重要?

HashMap 的重要性不在于“会不会用”,而在于:

  • 它综合了 数组、链表、红黑树 等经典数据结构

  • 它体现了 时间与空间的权衡

  • 它明确区分了 单线程容器 与 并发容器 的设计边界

  • 它的演进几乎映射了 Java 集合框架的成熟过程

理解 HashMap,往往是 Java 开发者从“API 使用者”走向“底层理解者”的一道分水岭。

JDK 1.2 ~ 1.7:草莽时代的 HashMap

JDK 1.0/1.1 只有 Hashtable,HashMap 是在 JDK 1.2 的 Collection Framework 中引入的。

底层结构

早期 HashMap 的底层结构非常直接:数组 + 链表

关键参数

Hash 计算方式

元素插入方式:头插法

  • JDK 1.2
package java.util;
import java.io.*;

public class HashMap<K, V> 
        extends AbstractMap<K, V> 
        implements Map<K, V>, Cloneable, Serializable 
{
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final Entry<?,?>[] EMPTY_TABLE = {};
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    transient int size;
    final float loadFactor;
    transient int modCount;
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
}
  • JDK 1.0

JDK 1.8:HashMap 的分水岭

如果说 HashMap 的历史只有一个转折点,那一定是 JDK1.8。

这一版本几乎重写了 HashMap 的核心逻辑。

底层结构升级:引入红黑树

插入方式改变:尾插法

更合理的 Hash 扰动函数

resize 机制的重大优化

JDK 9 ~ JDK 24:稳定后的演进期

在 JDK8 之后,HashMap 的核心设计已经趋于稳定。

可以认为:JDK8 定义了现代 HashMap 的最终形态。

HashMap 背后的设计哲学

回顾 HashMap 的演进,可以总结出几条非常典型的工程取舍。

1. 用空间换时间

负载因子 0.75

主动扩容

降低冲突概率

2. 为单线程 / 低并发场景优化

HashMap 明确不是线程安全的

并发问题交由 ConcurrentHashMap 解决

职责清晰,避免过度设计

3. 常规情况高效,极端情况兜底

链表应对大多数场景

红黑树兜底最坏复杂度 O(log n)