HashMap 源码分析

一、前言

HashMap 是 Java 中用于“键值映射”的高性能容器,适用于快速存取、数据映射和缓存等多种场景,是开发中最常用的数据结构之一。
本文将通过源码,让你从底层到使用全方位掌握HashMap的实现细节与设计考量。
本文是作者学习总结的文章,有错误的地方还请指出。


二、类结构与继承

2.1 类声明

在这里插入图片描述

继承自 AbstractMap,实现了 MapCloneableSerializable 等接口

2.2 关键内部类

Node<K,V>

在这里插入图片描述

静态内部类,实现 Map.Entry ,用于链表节点存储。

TreeNode<K,V>

在这里插入图片描述

JDK 8 新增,继承自 Node 并添加红黑树,用于高碰撞场景下的树化。

三、底层数据结构与常量

3.1 核心字段

/**
 * 哈希表主数组,存储键值对,首次使用时初始化。
 */
transient Node<K,V>[] table;

/**
 * 缓存 entrySet() 结果。
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 当前键值对数量。
 */
transient int size;

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

/**
 * 扩容阈值(容量 × 负载因子)。
 */
int threshold;

/**
 * 负载因子,决定扩容时机。
 */
final float loadFactor;

3.2 重要常量

/**
 * 默认初始容量,必须是 2 的幂,默认是 16。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * 最大容量,构造函数传入更大值时会使用此上限,必须是 2 的幂,最大为 2^30。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认负载因子,未指定时使用,默认是 0.75。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表转红黑树的阈值,单个桶中节点数 ≥ 8 时转换为树结构。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树退化为链表的阈值,节点数 ≤ 6 时转换回链表结构。
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 允许树化的最小哈希表容量,小于该值时即使桶中节点数达到 TREEIFY_THRESHOLD 也会先扩容而非树化。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

四、哈希与索引计算

4.1 扰动函数

/**
 * 计算键的哈希值,通过扰动函数(高位参与运算)来减少哈希冲突。
 * 如果 key 为 null,则哈希值为 0。
 *
 * @param key 键
 * @return 经过扰动处理后的哈希值
 */
static final int hash(Object key) {
    int h;
    // 使用高位扰动算法,防止哈希冲突集中在低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个方法的核心是通过 h ^ (h >>> 16) 这一扰动函数,把高位信息混合到低位,提高哈希值的随机性,从而使元素在数组中分布得更均匀,减少哈希冲突。

4.2 容量对齐与索引

/**
 * 返回大于等于给定容量 cap 的最小的 2 的幂次方值。
 * 例如,输入 10,返回 16。
 *
 * @param cap 期望的初始容量
 * @return 返回最小的满足条件的 2 的幂次方容量
 */
static final int tableSizeFor(int cap) {
    // 先减 1 是为了避免 cap 本身就是 2 的幂时多加一位
    int n = cap - 1; // n = cap - 1 = 9
    // 通过一系列位或运算将高位的1扩展到低位
    // 初始值 n = 9       ->    						0000 1001
    n |= n >>> 1;      //-> 0000 1001 | 0000 0100 = 0000 1101
    n |= n >>> 2;      //-> 0000 1101 | 0000 0011 = 0000 1111
    n |= n >>> 4;      //-> 0000 1111 | 0000 0000 = 0000 1111
    n |= n >>> 8;      //-> 0000 1111 | 0000 0000 = 0000 1111
    n |= n >>> 16;     //-> 0000 1111 | 0000 0000 = 0000 1111
    // 最后 n = 15,返回 n + 1 = 16
    // 若 n 小于 0,返回 1;若超过最大容量限制,返回最大容量;否则返回 n + 1(即下一个 2 的幂)
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法可以保证 HashMap 的内部数组长度始终是 2 的幂,便于通过位运算快速定位桶位置((n - 1) & hash),从而提升性能。

五、构造方法分析

// 指定初始容量和负载因子的构造方法
public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 如果初始容量大于最大容量,强制设为最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 负载因子必须为正且非NaN
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor;
    // 根据初始容量计算阈值(容量为大于等于指定值的最小2的幂)
    this.threshold = tableSizeFor(initialCapacity);
}

// 指定初始容量,使用默认负载因子(0.75)
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 无参构造方法,使用默认初始容量(16)和默认负载因子(0.75)
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他字段默认初始化
}

// 根据已有 Map 创建一个新的 HashMap,复制其所有映射关系
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false); // 将参数 map 中的键值对放入当前 HashMap 中
}

构造器功能说明
HashMap(int, float)指定容量和负载因子
HashMap(int)指定容量,默认负载因子
HashMap()默认容量和负载因子
HashMap(Map<K, V>)复制另一个 Map 的内容

无参构造延迟到首次 put 时初始化table,指定容量/因子可优化性能并减少扩容次数。

六、存储与扩容机制

6.1 putVal流程

// 向 HashMap 中插入或更新一个键值对
public V put(K key, V value) {
    // 计算 key 的扰动哈希值,并调用核心方法
    return putVal(hash(key), key, value, false, true);
}

/**
 * HashMap 的核心插入/更新逻辑
 *
 * @param hash          key 的哈希值(已扰动处理)
 * @param key           键
 * @param value         值
 * @param onlyIfAbsent  如果为 true,则已有值不替换
 * @param evict         控制后续操作(如缓存删除),创建模式下为 false
 * @return              旧值(如果替换了),否则返回 null
 */
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. 如果 table 还没初始化或长度为 0,先 resize() 分配初始数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 计算在数组中的槽位索引 i = (n-1) & hash
    if ((p = tab[i = (n - 1) & hash]) == null) {
        // 2.1 槽位为空,直接插入新节点
        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;
        }
        // 4. 如果该槽已经是红黑树结构,则调用树的插入逻辑
        else if (p instanceof TreeNode) {
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        }
        // 5. 否则遍历链表,查找或追加
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 5.1 到达链尾,追加新节点
                    p.next = newNode(hash, key, value, null);
                    // 5.2 如果链表长度超过阈值,触发树化
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 5.3 找到相同 key,退出循环准备更新
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break;
                }
                p = e;
            }
        }
        // 6. 如果找到已有节点 e,则根据 onlyIfAbsent 决定是否替换 value,返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 钩子方法,默认情况下不做任何操作,允许子类覆盖该方法,以支持扩展行为
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 7. 插入新节点后,更新修改计数和 size;若超过阈值,执行扩容
    ++modCount;
    if (++size > threshold)
        resize();
    // 钩子方法,默认情况下不做任何操作,允许子类覆盖该方法,以支持扩展行为
    afterNodeInsertion(evict);
    return null;
}
  • 初始化 table(若未分配)
  • 计算槽位索引并判断该位置是否为空
  • 解决链表冲突、链表→红黑树转换、红黑树插入
  • 更新已有值或新增节点(末尾增加)
  • 在插入后维护 modCountsize,并在超过 threshold 时触发扩容

6.2 扩容 (resize)流程

/**
 * 初始化或扩展 HashMap 的内部数组,并将旧数据迁移到新数组中。
 *
 * @return 扩容或初始化后的 table 数组
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 1. 如果已有容量
    if (oldCap > 0) {
        // 1.1 达到最大容量,不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2 否则容量翻倍,阈值也翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY) {
            newThr = oldThr << 1;
        }
    }
    // 2. 第一次初始化:如果 threshold 已存放初始容量,则直接用之
    else if (oldThr > 0) {
        newCap = oldThr;
    }
    // 3. 默认初始化路径:初始容量为 DEFAULT_INITIAL_CAPACITY
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 4. 若 newThr 仍未计算,则按 loadFactor 计算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY)
                 ? (int)ft
                 : Integer.MAX_VALUE;
    }
    threshold = newThr;

    // 5. 分配新数组并替换旧引用
    @SuppressWarnings("unchecked")
    Node<K,V>[] newTab = (Node<K,V>[]) new Node[newCap];
    table = newTab;

    // 6. 如果是扩容(oldTab 非空),则重新分配所有旧节点
    if (oldTab != null) {
        // 遍历旧数组每个槽
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e = oldTab[j];
            if (e == null) continue;
            // 释放旧引用 help GC
            oldTab[j] = null;                

            // 6.1 单节点:直接放入新数组对应位置
            if (e.next == null) {
                int idx = e.hash & (newCap - 1);
                newTab[idx] = e;
            }
            // 6.2 红黑树节点:调用 split 逻辑分裂到两边
            else if (e instanceof TreeNode) {
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            }
            // 6.3 普通链表:按 hash 高位分为低位链和高位链
            else {
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    // 保存下一节点
                    next = e.next;           
                    // 判断当前节点应留在原索引 (低位) 还是搬到原索引+oldCap (高位)
                    // 低位
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null) loHead = e;
                        else loTail.next = e;
                        loTail = e;
                    } else {
                    	// 高位
                        if (hiTail == null) hiHead = e;
                        else hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);

                // 将低位链挂到 newTab[j]
                if (loTail != null) {
                	// 断开链尾
                    loTail.next = null;    
                    newTab[j] = loHead;
                }
                // 将高位链挂到 newTab[j + oldCap]
                if (hiTail != null) {
                	// 断开链尾
                    hiTail.next = null;    
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
    return newTab;
}
  • 计算新的容量和阈值
  • 分配新数组并替换
  • 将旧数据重新分配到新数组中,依据 hash 的高位进行划分(低位不变,高位映射到新位置)

七、读取与查找

// 根据 key 获取 value
public V get(Object key) {
    Node<K,V> e;
    // 调用 getNode 获取节点,如果不为空就返回其 value,否则返回 null
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// 根据 key 和 hash 查找对应的 Node 节点
final Node<K,V> getNode(int hash, Object key) {
	// Hash 表数组
    Node<K,V>[] tab; 
    // 首节点和临时节点
    Node<K,V> first, e; 
    // Hash 表长度
    int n; 
    K k;

    // 检查 table 是否初始化 且 长度大于0 且 该 hash 对应的 bucket 有值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        // 检查首节点是否就是目标节点
        if (first.hash == hash && ((k = first.key) == key || key.equals(k)))
            return first;

        // 若不是首节点,检查链表或红黑树中的后续节点
        if ((e = first.next) != null) {
            // 如果是红黑树节点,调用红黑树查找方法
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

            // 遍历链表查找
            do {
                if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                    return e;
            } while ((e = e.next) != null);
        }
    }

    // 没找到返回 null
    return null;
}

get() 会根据 keyhash 值快速定位到桶,然后根据 hashequals 方法查找目标 key。桶中元素如果较少用链表,否则自动转换为红黑树查找。

八、删除操作

// 根据 key 删除对应的节点,并返回 value
public V remove(Object key) {
    Node<K,V> e;
    // 调用 removeNode 删除节点,返回被删除节点的 value
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * 移除指定 key 对应的节点(键值对)
 *
 * @param hash        key 的哈希值
 * @param key         要移除的键
 * @param value       仅当 matchValue 为 true 时才用于判断是否移除(一般传 null)
 * @param matchValue  是否需要同时匹配 value(比如用于替换时的条件删除)
 * @param movable     是否允许移动节点(在 resize 等场景中可能不允许移动)
 * @return 被移除的节点,如果不存在则返回 null
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab;        // Hash 表
    Node<K,V> p;            // 当前桶的首节点
    int n, index;

    // 如果 table 为空 或者 长度为 0 或 桶中首节点为空,说明 key 不存在
    if ((tab = table) == null || (n = tab.length) == 0 ||
        (p = tab[index = (n - 1) & hash]) == null)
        return null;

	// node 是目标节点,e 是临时变量
    Node<K,V> node = null, e; 
    K k; V v;

    // 检查首节点是否就是要删除的目标节点
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        node = p;

    // 如果不是首节点,则遍历红黑树或链表查找目标节点
    else if ((e = p.next) != null) {
        if (p instanceof TreeNode)
        	// 红黑树查找
            node = ((TreeNode<K,V>)p).getTreeNode(hash, key); 
        else {
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    node = e;
                    break;
                }
                // 记录上一个节点,用于链表删除操作
                p = e; 
            } while ((e = e.next) != null);
        }
    }

    // 如果没找到目标节点,返回 null
    if (node == null) return null;

    // 红黑树节点,调用红黑树删除方法
    if (node instanceof TreeNode)
        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

    // 删除的是首节点,直接替换为 node.next
    else if (node == p)
        tab[index] = node.next;

    // 中间节点或尾节点,使用前驱节点 p 进行断链
    else
        p.next = node.next;
	
	// 修改次数 +1,用于快速失败的迭代器
    ++modCount;
    // 实际大小 -1         
    --size;
    // 留给子类(如 LinkedHashMap)复写的方法             
    afterNodeRemoval(node); 
    return node;
}

九、碰撞处理与树化

  • 链表方式:默认在桶尾插入新节点,性能退化时为 O(n)
  • 树化条件:单桶链表长度 ≥ TREEIFY_THRESHOLD (8) 且 table.length ≥ MIN_TREEIFY_CAPACITY (64),调用 treeifyBin() 将链表转为红黑树
  • 退树:当树节点个数降至 UNTREEIFY_THRESHOLD (6) 以下时,恢复链表

十、迭代器与 Fail‑Fast

10.1 什么是 Fail-Fast?

Fail-Fast 是一种快速失败机制:当多个线程对同一个集合进行结构性修改(非线程安全)时,若集合的迭代器检测到结构被改变,会立刻抛出异常,防止出现不一致状态或数据错误。

10.2 为什么需要 Fail-Fast?

因为 HashMap 不是线程安全的,如果一个线程正在遍历它,另一个线程修改了结构(如新增/删除元素),可能导致:

  • 死循环
  • 元素遗漏
  • 数据不一致

为了避免这些严重问题,Java 设计了 Fail-Fast 机制,通过比较结构修改次数,一旦发现异常立刻中止操作。

10.3 modCountexpectedModCount

  • modCount记录结构性修改的次数
  • expectedModCount:当我们通过 HashMapkeySet()entrySet() 等方法创建迭代器时,迭代器内部会保存一份副本,期望的修改次数

10.4 如何触发 Fail-Fast?

  • 当迭代器调用 next()remove() 的时候,都会校验
  • 只要 HashMap 的结构被其他线程修改(或者同一线程未通过迭代器的 remove() 删除),modCount 就会变,但迭代器内部 expectedModCount 没变,就会抛异常。
if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

10.5 哪些操作会修改 modCount?

结构性修改(结构发生变化的操作)

  • put(K, V)
  • remove(K)
  • clear()
  • resize()

10.6 正确的删除方法

Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    if ("a".equals(key)) {
    	// 不会触发 ConcurrentModificationException
        it.remove(); 
    }
}

十一、JDK 7 与 JDK 8 的主要变化

11.1 数据结构的变化

版本底层结构冲突解决特性
JDK 7数组 + 链表采用链表(拉链法)解决冲突不支持树化
JDK 8数组 + 链表 + 红黑树冲突较多时链表转为红黑树支持链表树化,提升查询效率
  • JDK 7 中:所有冲突元素插入链表头部(头插法)。
  • JDK 8 中:冲突元素插入链表尾部(尾插法),并引入红黑树结构。

11.2 Hash 函数优化(扰动函数)

  • JDK 7:使用 key.hashCode() 后直接与桶长度取模(index = hashCode % length)
  • JDK 8:引入 扰动函数(hash spreading),通过高位与低位异或 + 右移,使 hash 分布更均匀,解决 hashCode() 不均匀时的桶偏斜问题。

11.3 树化机制(链表转红黑树)

  • 触发条件:桶中元素个数 > 8(TREEIFY_THRESHOLD)且数组长度 >= 64(MIN_TREEIFY_CAPACITY
  • 好处:
    • 链表查询时间复杂度:O(n)
    • 红黑树查询时间复杂度:O(log n)
    • 在高 hash 冲突场景下显著提升性能

11.4 并发安全改进(虽仍非线程安全)

项目JDK 7JDK 8
插入方式头插尾插
扩容方式扩容时将链表重新散列,顺序不变resize 时将链表拆成低位链和高位链,保持原顺序
并发扩容多线程扩容可能形成死循环(链表成环)更安全稳定,避免死循环

十二、与其他 Map 实现的对比

特性HashMapHashtableLinkedHashMapConcurrentHashMap
线程安全是(同步)是(分段锁/CAS)
允许 null 键/值
碰撞处理链表/红黑树链表链表/红黑树链表/红黑树
遍历顺序不保证不保证插入/访问顺序不保证
扩容因子1.5×同 HashMap动态

十三、常见面试题

待定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值