1.1 HashMap (JDK1.8) 源码解析

1.1.1 底层存储结构原理

JDK1.8 采用 数组 (Node \[\]) + 单向链表 + 红黑树 三层结构,解决 JDK7 纯链表查询慢问题:

  1. 数组:哈希桶,长度固定为 2 的幂,快速定位元素;
  2. Node 单向链表:哈希冲突时挂载元素;
  3. TreeNode 红黑树:链表长度≥8 转为红黑树,查询时间复杂度从 O (n) 降到 O (logn);
  4. 转换阈值:链表≥8 转红黑树,节点≤6 退回链表;7 作为缓冲阈值,避免频繁转换造成性能抖动。
Node 节点源码 + 逐行解释
java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    // 当前key经过扰动函数生成哈希值,final不可变,避免修改导致下标错乱
    final int hash;
    // 键不可变,重写equals时依据key对比
    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;
    }

    // 获取键值拼接字符串,打印输出用
    public final String toString() {
        return key + "=" + value;
    }

    // 哈希值 = key哈希 ^ value哈希,保证Entry整体唯一性
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    // 修改value并返回旧值
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // 对比两个Entry是否完全相等(key、value全部一致)
    public final boolean equals(Object o) {
        if (this == o) return true; // 自身直接相等
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            // key相等 且 value相等才算同一个Entry
            return Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue());
        }
        return false;
    }
}

文字解释

  • hash、key 被 final 修饰:节点创建后键与哈希无法修改,防止修改后元素存储下标错乱、内存丢失;
  • next 指针形成单向链表,同一哈希桶下冲突元素串联;
  • equals 同时对比 key 和 value,hashCode使用异或保证键值组合哈希唯一。
HashMap 核心成员变量
java 复制代码
transient Node<K,V>[] table; // 哈希主数组,也称哈希桶,延迟初始化(首次put才创建)
transient int size; // 当前存储键值对总数量
transient int modCount; // 修改计数器,用于快速失败fail-fast机制
int threshold; // 扩容阈值 = 数组容量 * 负载因子,size超过触发resize扩容
final float loadFactor; // 负载因子,默认0.75,平衡空间与查询效率

文字解释

  1. transient:序列化时忽略数组,序列化会单独处理键值对,节省序列化空间;
  2. modCount:新增 / 删除 / 修改时自增,迭代过程 modCount 变化直接抛ConcurrentModificationException
  3. threshold:数组装满 75% 时扩容,避免哈希冲突激增。

1.1.2 扰动函数 hash () 原理 + 代码

java 复制代码
static final int hash(Object key) {
    int h;
    // key为null直接返回0,存入数组下标0;
    // key非空:原始hashCode右移16位,与自身异或,混合高低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

文字完整解释

  1. 问题:原始hashCode()仅低 16 位参与(n-1)&hash下标计算,高位完全失效,大量 key 映射同一桶,冲突严重;
  2. 解决方案:将 hashCode 高 16 位右移到低 16 位,与原值异或,让低位携带高位特征
  3. 效果:大幅分散哈希值,降低哈希碰撞概率;
  4. JDK7 对比:4 次移位异或,运算开销更大,JDK8 简化为一次运算,性能提升。

1.1.3 put () 完整源码逐行解析

scss 复制代码
public V put(K key, V value) {
    // 封装底层putVal,传入扰动后的hash、键、值
    return putVal(hash(key), key, value, false, true);
}

/**
 * @param hash 扰动后的哈希
 * @param key 存入键
 * @param value 存入值
 * @param onlyIfAbsent true:存在key不覆盖,false:直接覆盖
 * @param evict 预留缓存淘汰标记,HashMap无用,LinkedHashMap使用
 */
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. 判断哈希数组未初始化/长度为0,执行resize创建默认容量16的数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 计算存储下标 i = (数组长度-1) & hash 等价 hash % n(位运算更快)
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 3. 当前桶无元素,直接新建Node放入数组
        tab[i] = newNode(hash, key, value, null);
    else {
        // 当前桶存在元素,发生哈希冲突
        Node<K,V> e; K k;
        // 4. 桶头节点key完全匹配,属于更新操作
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 5. 桶头是红黑树节点,走红黑树插入逻辑
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 6. 普通单向链表场景,循环遍历链表尾部新增
        else {
            for (int binCount = 0; ; ++binCount) {
                // 遍历到链表末尾,无重复key,新建节点追加
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 链表长度≥8,调用treeifyBin转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 遍历中找到相同key,跳出循环准备覆盖value
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 7. 存在旧key,执行覆盖逻辑
        if (e != null) {
            V oldValue = e.value;
            // onlyIfAbsent=false允许覆盖原值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // LinkedHashMap回调,HashMap空实现
            return oldValue; // 返回旧值
        }
    }
    ++modCount; // 修改计数+1,迭代检测快速失败
    // 8. 元素数量超过扩容阈值,执行resize两倍扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // LinkedHashMap淘汰回调
    return null; // 新增元素返回null
}

文字分步总结流程

  1. 数组为空 → 初始化容量 16;

  2. 计算下标,空桶直接存入;

  3. 桶有元素:

    • 头 key 匹配:覆盖 value;
    • 红黑树节点:树插入;
    • 链表:尾部追加,长度≥8 转红黑树;
  4. 插入完成,size 超过阈值扩容。

1.1.4 get () 源码与逻辑说明

kotlin 复制代码
public V get(Object key) {
    Node<K,V> e;
    // 计算hash,调用getNode查找节点,无节点返回null
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 数组非空,且对应桶存在头节点
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 头节点直接匹配key,直接返回
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 头不匹配,存在后续节点
        if ((e = first.next) != null) {
            // 红黑树,调用树查询方法
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 遍历单向链表匹配key
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 无匹配元素返回null
    return null;
}

文字解释

  1. 先定位哈希桶,优先判断桶头,命中直接返回;
  2. 桶头不区分红黑树 / 链表分别遍历查找;
  3. 找不到返回 null,无法区分 key 不存在 /value 为 null。

1.1.5 Hash 冲突完整讲解

  1. 定义 :不同 key 经过hash()+(n-1)&hash计算后,落到同一个数组下标;
  2. 产生原因:hashCode 范围 -2\^31,2\^31-1,数组长度仅 16/32,取模必然重叠;
  3. 缓解手段:扰动函数混合高低位,分散哈希值;
  4. 解决手段:链地址法(链表 + 红黑树);
  5. 举例:数组长度 8,hash=9、hash=17,9&7=117&7=1,同下标冲突。

1.1.6 为什么 HashMap 容量必须是 2 的 n 次方

核心公式:i = (table.length - 1) & hash

  1. 性能层面 :位运算&比取模%CPU 运算快数倍;只有 2^n 时,length-1二进制全 1,&等价取模;
  2. 减少冲突 :若长度非 2 次幂,length-1存在二进制 0 位,hash 对应 bit 会置 0,一半数组空间永久无法使用,冲突暴增;
  3. 底层tableSizeFor方法自动向上取最近 2 次幂:
ini 复制代码
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 >= MAX_CAPACITY) ? MAX_CAPACITY : n + 1;
}

代码解释:不断右移或运算,把低位全部填充 1,最后 + 1 得到最近 2 次幂。

1.1.7 负载因子 0.75 设计原理

负载因子 = 存储元素 size / 数组容量 capacity

  1. 负载越大:数组利用率高,内存省,但哈希冲突变多,链表变长查询慢;
  2. 负载越小:冲突少查询快,但大量数组空间闲置,内存浪费;
  3. 0.75 是统计学最优折中,平衡时间与空间开销,JDK 官方实测最优值。

1.1.8 resize 扩容机制

  1. 扩容规则:新容量 = 原容量 × 2,保证 2 次幂;
  2. 下标优化:hash & oldCap == 0下标不变,否则原下标+oldCap
  3. JDK8 优化:链表尾插,JDK7 头插多线程会形成环形链表 CPU100% 死循环;
  4. 扩容开销极大,开发建议初始化new HashMap(预期容量/0.75),减少扩容次数。

1.1.9 HashMap vs Hashtable vs ConcurrentHashMap

表格

特性 HashMap Hashtable ConcurrentHashMap
线程安全 ❌ 非线程安全 ✅ 线程安全(synchronized) ✅ 线程安全(CAS + synchronized)
效率 低(全表锁) 高(分段锁 / 桶级锁)
null 键 / 值 允许 1 个 null 键,多个 null 值 不允许 null 键 / 值 不允许 null 键 / 值
初始容量 16 11 16
扩容方式 2 倍 2 倍 + 1 2 倍
锁机制 全局锁 分段锁(JDK1.7)/ CAS + synchronized(JDK1.8+)
适用场景 单线程环境 已过时,不推荐使用 多线程并发环境
出现版本 JDK 1.2 JDK 1.0 JDK 1.5
父类 AbstractMap Dictionary AbstractMap
迭代器 fail-fast fail-fast(Enumeration 不是) fail-safe(弱一致性)

1.1.10 HashMap 线程不安全三大场景

  1. 并发 put/remove:数据覆盖、元素丢失;
  2. 迭代时修改集合,modCount 不一致抛出ConcurrentModificationException快速失败;
  3. JDK7 扩容头插,多线程环形链表死循环;解决方案 :并发使用ConcurrentHashMap,单线程使用 HashMap。

1.1.11 Key 对象规范

必须使用不可变对象(String、Integer):

  1. 可变实体修改属性后 hashCode 改变,元素存储下标变更,get 无法找到数据;
  2. 自定义类做 key 必须重写hashCode()equals()
相关推荐
爱勇宝4 小时前
小红花成长新版:模板来了,鼓励也更容易开始
前端·后端·程序员
竹林8184 小时前
Solana前端开发:我在一个NFT铸造页面上被@solana/web3.js的Connection和Transaction签名坑了两天
前端
冬奇Lab5 小时前
每日一个开源项目(第144篇):ai-website-cloner-template - 一条命令、多 Agent 并行,把任意网站逆向成 Next.js 代码
前端·人工智能·开源
玄玄子5 小时前
webpack publicPath作用原理
前端·webpack·程序员
HduSy5 小时前
帮 Claude Code 做了个菜单栏 Token 看板,聊聊里面的一些实现逻辑
前端
用户059540174465 小时前
用了6个月LangChain,才发现AI Agent的记忆存储一直有坑——写了23个Pytest用例才彻底修好
前端·css
奶油mm5 小时前
我偷偷把公司的祖传 jQuery 项目改成了 Vue3,CTO 没发现,但全组都来抄我的代码了
前端
用户2136610035725 小时前
Vue2非父子通信与动态组件
前端·vue.js
PedroQue995 小时前
Vite插件体系1.0.0:API稳定,生产就绪
前端·vite