为什么 HashMap 是 Java 中最常用、最重要的数据结构?

核心原因就一个:性能。常见的基础数据结构中,数组查询快但插入删除慢,链表插入快但查询慢。HashMap 综合了数组和链表的优点,将查询与插入的效率都控制在近似 O(1) 的复杂度内。

但 HashMap 的设计远不止于此。容量为什么是 2 的幂?哈希扰动函数 spread() 如何防止哈希冲突?链表转红黑树的阈值为什么选 8?本文将从源码级别逐一揭示这些设计背后的原理。

学完本文,你将掌握:

  1. HashMap 的底层实现原理(数组 + 链表 + 红黑树)
  2. put 方法的完整执行流程与哈希扰动算法
  3. 扩容机制与 2 倍扩容的根本原因
  4. HashMap 为什么线程不安全?并发下会产生什么事故?
  5. HashMap 的容量为什么必须是 2 的幂?
  6. HashMap 在 Java 8 版本中做了哪些变更?

简介

HashMap 的底层数据结构由数组、链表和红黑树组成,核心是基于数组实现的。为了解决哈希冲突,采用拉链法,于是引入了链表结构。为了解决链表过长导致的查询性能下降,Java 8 引入了红黑树结构。

HashMap 类中有两个关键的阈值常量:

  • TREEIFY_THRESHOLD = 8:当链表长度达到 8 时,链表会转换为红黑树
  • UNTREEIFY_THRESHOLD = 6:当红黑树节点数减少到 6 时,红黑树会退化为链表
  • MIN_TREEIFY_CAPACITY = 64:只有数组容量达到 64 时才会触发树化,否则优先扩容数组而不是树化

这三个常量配合使用,目的是避免频繁的树化和退化操作。如果链表只是短暂变长,不会触发树化;如果红黑树只是短暂变小,不会立即退化。

💡 核心提示:为什么树化阈值是 8、退化阈值是 6?这背后有统计学依据。HashMap 中节点的分布服从泊松分布(Poisson Distribution)。在理想的 hash 散列下,链表长度达到 8 的概率约为 0.00000006(千万分之六),几乎不可能自然发生。如果达到了 8,说明 hash 冲突严重,此时用红黑树代替链表可以将 O(n) 退化为 O(log n)。退化为 6 是为了留出缓冲区间(8→6),避免在临界值附近频繁树化/退化,这就是所谓的"迟滞效应"(Hysteresis)。

HashMap 类架构图

contains

implements

implements

«abstract»

AbstractMap

+containsKey(Object) : boolean

+containsValue(Object) : boolean

+isEmpty() : boolean

+size() : int

+clear() : void

«interface»

Map

+put(K, V) : V

+get(Object) : V

+remove(Object) : V

+containsKey(Object) : boolean

+containsValue(Object) : boolean

+entrySet() : Set<Entry>

+keySet() : Set<K>

+values() : Collection<V>

HashMap

-transient Node[] table

-transient int size

-transient int threshold

-transient int modCount

-final float loadFactor

+put(K, V) : V

+get(Object) : V

+remove(Object) : V

+resize() : Node[]

-hash(Object) : int

-putVal(int, K, V, boolean, boolean) : V

-getNode(int, Object) : Node

-treeifyBin(Node[], int) : void

-tableSizeFor(int) : int

LinkedHashMap

-transient Entry head

-transient Entry tail

-final boolean accessOrder

Node

+final int hash

+final K key

+V value

+Node next

+getKey() : K

+getValue() : V

+setValue(V) : V

TreeNode

+TreeNode parent

+TreeNode left

+TreeNode right

+TreeNode prev

+boolean red

+putTreeVal(HashMap, Node[], int, K, V) : TreeNode

+getTreeNode(int, Object) : TreeNode

+removeTreeNode(HashMap, Node[], boolean) : void

+split(HashMap, Node[], int, int) : void

«interface»

Cloneable

«interface»

Serializable

HashMap 的核心工作原理可以用下面的流程图概括:

红黑树

链表

初始化: table=null, size=0, threshold=0

put(key, value)

hash(key): h = key.hashCode()\n返回 h ^ (h >>> 16)

table 为空?

resize(): 初始化数组\n容量=16 或 tableSizeFor(指定容量)

计算下标: i = (n - 1) & hash

tab[i] 是否为空?

直接插入新节点

key 是否已存在?

覆盖旧值, 返回旧值

节点类型?

putTreeVal: 红黑树插入

遍历链表, 尾插法追加

链表长度 >= 8?

数组容量 >= 64?

treeifyBin: 链表转红黑树

resize(): 扩容数组

size > threshold?

resize(): 容量翻倍\n链表拆分为低位/高位两条链表

modCount++, size++, 返回null

get(key)

hash(key) 计算哈希值

计算下标: (n-1) & hash

找到 key?

返回 value

返回 null

类属性

再看一下 HashMap 类中有哪些关键属性:

public class HashMap<K, V> extends AbstractMap<K, V>
        implements Map<K, V>, Cloneable, Serializable {

    /**
     * 默认容量大小,1 << 4 = 16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /**
     * 负载系数,容量超过 threshold = capacity * loadFactor 时触发扩容
     * 默认 16 * 0.75 = 12 个元素时扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 容量最大值,2 的 30 次方
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 链表转红黑树的阈值:链表长度 > 8 时树化
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树退化为链表的阈值:节点数 < 6 时退化
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 树化的最小数组容量:只有 table 容量 >= 64 才会树化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 存储元素的数组,容量始终为 2 的幂
     */
    transient Node<K, V>[] table;

    /**
     * 实际存储的键值对数量
     */
    transient int size;

    /**
     * 扩容阈值,当 size > threshold 时触发扩容
     */
    transient int threshold;

    /**
     * 结构修改次数,用于 fail-fast 机制
     */
    transient int modCount;
}

其中 Node 是链表的节点:

static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;    // 哈希值(缓存,避免重复计算)
    final K key;       // 键
    V value;           // 值
    Node<K, V> next;   // 后继节点

    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

以及红黑树的节点 TreeNode(继承自 LinkedHashMap.Entry,本质是 Node 的子类):

static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
    TreeNode<K, V> parent;  // 父节点
    TreeNode<K, V> left;    // 左子节点
    TreeNode<K, V> right;   // 右子节点
    TreeNode<K, V> prev;    // 前驱节点(继承自 LinkedHashMap.Entry)
    boolean red;            // 节点颜色

    TreeNode(int hash, K key, V val, Node<K, V> next) {
        super(hash, key, val, next);
    }
}

初始化

HashMap 常见的初始化方法有两个:

  1. 无参初始化
  2. 有参初始化,指定容量大小
/**
 * 无参初始化
 */
Map<Integer, Integer> map = new HashMap<>();
/**
 * 有参初始化,指定容量大小
 */
Map<Integer, Integer> map = new HashMap<>(10);

再看一下构造方法的底层实现:

/**
 * 无参初始化
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
 * 有参初始化,指定容量大小
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 有参初始化,指定容量大小和负载系数
 */
public HashMap(int initialCapacity, float loadFactor) {
    // 校验参数
    if (initialCapacity < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    }
    if (initialCapacity > MAXIMUM_CAPACITY) {
        initialCapacity = MAXIMUM_CAPACITY;
    }
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    }
    this.loadFactor = loadFactor;
    // 计算出合适的容量大小(2的幂)
    this.threshold = tableSizeFor(initialCapacity);
}

可以看出,无参构造方法只初始化了负载系数。指定容量大小的有参构造方法也只是初始化了负载系数和 threshold(扩容阈值),两个方法都没有初始化数组大小

tableSizeFor 方法会将传入的容量值转换为大于等于它的最小的 2 的幂。例如 tableSizeFor(10) 返回 16,tableSizeFor(20) 返回 32。这个方法实现非常精妙:

/**
 * 返回大于等于 cap 的最小的 2 的幂
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

💡 核心提示tableSizeFor 方法为什么先 cap - 1?如果不减 1,当 cap 本身就是 2 的幂时(比如 16),结果会变成 32,导致浪费一半空间。减 1 保证了 "恰好等于 2 的幂" 时不会翻倍。而右移 + 或运算的组合,能将最高位 1 右侧的所有位都填充为 1,最后 +1 就是下一个 2 的幂。

如果再有面试官问你,HashMap 初始化的时候数组大小是多少?答案是 0(或者说未初始化),因为 HashMap 采用了懒加载策略,数组在第一次 put 时才会通过 resize() 方法初始化。

深入 hash 算法

在深入 put 方法之前,先看一下 HashMap 的 hash() 方法:

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

这个方法做了两件事:

  1. 如果 key 为 null,返回 0(HashMap 允许 null 作为 key)
  2. 否则,取 key 的 hashCode(),然后将其无符号右移 16 位,再与原值做异或运算

为什么要这样做?HashMap 计算数组下标用的是 (n - 1) & hash,其中 n 是数组容量(2 的幂)。当 n 较小时(比如 16),n - 1 只有低 4 位是 1,高位都是 0,这意味着 & 运算只会用到 hash 的低位。如果不同 key 的 hashCode 只有高位不同、低位相同,就会产生哈希冲突。通过右移 16 位再异或,把高位的信息混入低位,让高位也参与下标计算,从而减少哈希冲突

💡 核心提示:为什么选择右移 16 位?因为 int 是 32 位,右移 16 位恰好将高 16 位与低 16 位对齐,异或后高低位的信息混合在一起。这是一个在性能和散列质量之间的权衡:更多次的混合(比如多次右移+异或)可以提高散列质量,但会降低性能;16 位一次的混合在实践中已足够好。

put 源码

put 方法的流程如下:

  1. 计算 key 的 hash 值
  2. 如果数组为空,调用 resize() 初始化
  3. 根据 (n - 1) & hash 计算数组下标
  4. 如果下标位置为空,直接插入
  5. 如果下标位置不为空,判断 key 是否已存在,存在则覆盖
  6. 如果是红黑树节点,执行红黑树插入
  7. 如果是链表节点,遍历链表尾插,长度达到 8 时树化
  8. 如果 size > threshold,调用 resize() 扩容

put 方法详细流程图

put(key, value)

hash(key) 计算扰动后的哈希值

table 是否为空?

resize() 初始化数组

i = (n-1) & hash 计算下标

tab[i] == null?

tab[i] = newNode() 直接插入

p.hash == hash && key匹配?

e = p 记录待覆盖节点

p instanceof TreeNode?

putTreeVal() 红黑树插入

遍历链表尾插

binCount >= 7?

table.length >= 64?

treeifyBin() 链表转红黑树

resize() 扩容数组

++size > threshold?

覆盖旧值, 返回旧值

resize() 扩容

modCount++, size++, 返回null

返回旧值

put 源码详解

再看一下 put 方法的具体源码实现:

/**
 * put 方法入口
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 计算 hash 值(高位和低位都参与计算)
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * 实际的put方法逻辑
 * @param hash key对应的hash值
 * @param key 键
 * @param value 值
 * @param onlyIfAbsent 如果为true,则只有当key不存在时才会put,否则会覆盖
 * @param evict 如果为false,表处于创建模式
 * @return 返回旧值
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;
    // 1. 如果数组为空,则执行初始化(resize 同时负责初始化和扩容)
    if ((tab = table) == null || (n = tab.length) == 0) {
        n = (tab = resize()).length;
    }
    // 2. 如果 key 对应下标位置元素不存在,直接插入即可
    if ((p = tab[i = (n - 1) & hash]) == null) {
        tab[i] = newNode(hash, key, value, null);
    } else {
        Node<K, V> e;
        K k;
        // 3. 如果头节点 key 匹配,直接结束,后续判断是否需要覆盖
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) {
            e = p;
        } else if (p instanceof TreeNode) {
            // 4. 判断下标位置的元素类型,如果是红黑树,则执行红黑树的插入逻辑
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        } else {
            // 5. 否则执行链表的插入逻辑
            for (int binCount = 0; ; ++binCount) {
                // 6. 遍历链表,直到找到空位置为止
                if ((e = p.next) == null) {
                    // 7. 创建一个新的链表节点,并追加到末尾(尾插法)
                    p.next = newNode(hash, key, value, null);
                    // 8. 如果链表长度达到 8,则转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) {
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                // 9. 如果在链表中找到相同的 key,则结束
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break;
                }
                p = e;
            }
        }
        // 10. 判断是否需要覆盖旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;
            }
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 11. 判断是否需要扩容
    if (++size > threshold) {
        resize();
    }
    afterNodeInsertion(evict);
    return null;
}

其中 treeifyBin 方法负责将链表转换为红黑树,但只有在数组容量达到 MIN_TREEIFY_CAPACITY(64)时才会真正树化,否则只是扩容数组:

final void treeifyBin(Node<K, V>[] tab, int hash) {
    int n, index;
    Node<K, V> e;
    // 如果数组容量 < 64,优先扩容而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
        resize();
    } else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 否则,将链表转换为红黑树
        TreeNode<K, V> hd = null, tl = null;
        // 遍历链表,将所有 Node 替换为 TreeNode
        do {
            TreeNode<K, V> p = new TreeNode<>(e.hash, e.key, e.value, null);
            if (tl == null) {
                hd = p;
            } else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 将 TreeNode 双向链表构造成红黑树
        if ((tab[index] = hd) != null) {
            hd.treeify(tab);
        }
    }
}

更多推荐