前言
HashMap 是 Java 集合框架中最核心、面试频率最高的类,没有之一。无论是日常业务开发,还是后端技术面试,HashMap 的底层原理都是绕不开的话题。
很多人知道它是数组 + 链表 + 红黑树的结构,但对 put 流程、扩容机制、树化逻辑一知半解。本文从 JDK8 源码出发,由浅入深拆解 HashMap 的底层实现,并附上实战性能优化建议。
一、底层数据结构:从 JDK7 到 JDK8 的演进

  1. JDK7 的结构
    JDK7 及之前,HashMap 底层是数组 + 单向链表。
    元素通过哈希计算得到数组下标,如果多个元素哈希冲突(下标相同),就以链表的形式挂在数组对应位置上,采用头插法插入新元素。
    缺点:哈希冲突严重时链表会越来越长,查询效率从 O (1) 退化成 O (n);并发环境下扩容容易形成环形链表,导致死循环。
  2. JDK8 的结构
    JDK8 对 HashMap 做了重大优化,底层变为数组 + 链表 + 红黑树。
    当链表长度小于 8 时,保持链表结构;
    当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表会转化为红黑树,查询效率提升为 O (logn);
    当元素删除,树节点数 ≤ 6 时,红黑树会退化为链表。
    插入方式改为尾插法,解决了并发扩容的死循环问题(注意:HashMap 依然不是线程安全的)。
    二、核心参数与常量
    理解 HashMap,先搞懂这几个关键参数:
    核心参数与常量
    经典问题:为什么容量必须是 2 的 n 次方?
    计算数组下标时用的是 hash & (length - 1),当 length 是 2 的 n 次方时,length-1 的二进制全是 1,与运算结果等价于取模,效率更高,且能保证哈希分布更均匀。
    三、put 方法完整执行流程
    put 是 HashMap 最核心的方法,整个流程可以拆解为 7 步:
    计算哈希值:对 key 的 hashCode 做扰动计算 (h = key.hashCode()) ^ (h >>> 16),让高位也参与运算,减少哈希冲突;
    判空初始化:如果数组为空,调用 resize() 初始化数组,默认长度 16;
    计算数组下标:通过 hash & (length - 1) 得到桶位下标;
    判断桶位状态:
    如果该位置没有元素,直接新建节点放入桶中;
    如果该位置有元素,判断 key 是否相同,相同则覆盖 value;
    如果是红黑树节点,调用红黑树的插入方法;
    如果是链表,遍历链表尾插,遍历中发现 key 相同则覆盖;
    判断是否树化:链表插入完成后,若链表长度 ≥ 8,调用 treeifyBin() 尝试树化(数组长度不足 64 则先扩容);
    元素计数 + 1:元素数量 size 加 1;
    判断是否扩容:如果 size > 阈值 threshold,调用 resize() 进行扩容。
    四、扩容机制:JDK8 的精妙优化
    扩容是 HashMap 最复杂的部分,也是面试高频考点。
  3. 什么时候扩容?
    两种情况触发扩容:
    元素数量 size > 阈值(容量 × 加载因子);
    链表长度达到 8,但数组长度小于 64,优先扩容而不是树化。
  4. 扩容做了什么?
    数组长度变为原来的 2 倍,阈值也变为原来的 2 倍;
    遍历旧数组的每个桶位,将元素重新计算位置放入新数组;
    链表元素拆分时,JDK8 做了关键优化:不需要重新计算 hash。
    因为容量是 2 倍扩容,元素的新位置要么是原下标,要么是原下标 + 旧容量。只需要判断 hash 值对应高位 bit 是 0 还是 1,就能直接确定新位置,省去了重新计算 hash 的开销,同时避免了 JDK7 头插法导致的链表倒置和死循环问题。
    五、实战性能优化建议
    了解原理最终是为了写出更好的代码,日常开发中可以从这几点优化 HashMap 的性能:
  5. 合理设置初始容量
    HashMap 默认初始容量 16,如果我们预知要存入大量元素,一定要在构造时指定初始容量,避免频繁扩容带来的性能损耗。
    计算公式推荐:初始容量 = 预期元素数量 / 0.75 + 1
    比如预计存 100 个元素,100 / 0.75 ≈ 133,向上取 2 的幂就是 128 不够,选 256。
    // 反例:插入1000个元素,会触发多次扩容
    HashMap<String, Object> map = new HashMap<>();

// 正例:预设容量,减少扩容
HashMap<String, Object> map = new HashMap<>(1024);
2. 不要随意修改加载因子
默认 0.75 是时间和空间的平衡值,不建议随意修改:
加载因子太小:空间浪费严重,但哈希冲突少,查询快;
加载因子太大:空间利用率高,但哈希冲突多,查询变慢。
除非有特殊的空间或性能极致需求,否则保持默认 0.75 即可。
3. 优先用 String、Integer 等不可变类做 key
String、Integer 这类包装类天然不可变,重写了 hashCode 和 equals 方法,非常适合做 key。
如果用自定义对象做 key,必须重写 hashCode() 和 equals() 方法,否则会出现 “存进去取不出来” 的问题。
4. 并发场景不要用 HashMap
HashMap 是非线程安全的,并发环境下推荐:
读多写少场景:ConcurrentHashMap(推荐,性能最好);
不需要太高并发:Hashtable(全表锁,性能差,不推荐)。
总结
HashMap 是 Java 开发者的基本功,底层原理看似复杂,拆解后逻辑其实非常清晰。从数据结构到 put 流程,再到扩容机制,每一处设计都体现了工程师对性能和空间的权衡思考。
理解底层原理不仅是为了应付面试,更重要的是在日常开发中能合理使用、排查问题、优化性能。希望本文能帮你彻底吃透 HashMap。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐