深入Java集合框架:HashMap源码解析(JDK 8)

前言

在 Java 后端开发工程师的面试中,或者日常的高性能代码编写里,HashMap 永远是一个绕不开的话题。JDK 8 对 HashMap 进行了大刀阔斧的改造,引入了红黑树,彻底解决了 JDK 7 及以前版本中链表过长导致的性能瓶颈。

今天,我们就抛开表象,深入 java.util.HashMap 的源码,一层一层剥开它的设计精髓。


一、 核心数据结构:数组 + 链表 + 红黑树

在 JDK 8 之前,HashMap 的底层结构是 数组 + 链表 。而在 JDK 8 中,变成了 数组 + 链表 + 红黑树。

java 复制代码
// 核心数组,也称为哈希桶(Hash Bucket)
transient Node<K,V>[] table;

// 链表节点(当哈希冲突时,元素以链表形式挂载)
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next; // 指向下一个节点
}

// 红黑树节点(当链表长度达到一定阈值时转换)
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;
    boolean red;
}

为什么要引入红黑树?

在极端情况下(比如大量 Key 的 hashCode 都相同),链表会变得非常长。链表的查询时间复杂度是 O(n),而红黑树是 O(logn)。引入红黑树后,即使遭遇恶意构造的哈希碰撞攻击,HashMap 的性能也不会断崖式下跌。


二、 关键参数与阈值

在阅读源码前,必须先记住这几个核心常量,它们是 HashMap 运作的"灵魂":

面试考点:为什么链表转红黑树的阈值是 8?

根据泊松分布,在负载因子 0.75 的情况下,单个桶中节点数量达到 8 的概率小于千万分之一。也就是说,正常情况下链表不会变红黑树,一旦变了,说明数据分布极不均匀,红黑树能提供更好的查询性能。


三、 核心方法深度解析

1. 哈希函数:hash(Object key)

HashMap 没有直接使用 key.hashCode(),而是进行了一次**扰动函数(Perturbation function)**处理:

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

为什么要高 16 位异或低 16 位?

因为 HashMap 计算数组下标时用的是 (n - 1) & hash。当数组容量 n 较小时(比如 16),只有低 4 位参与了运算,高位的变化被忽略了。通过右移 16 位并与原哈希值异或,可以将高位的特征混合到低位,从而减少哈希冲突。


2. 存入元素:put(K key, V value)

put 方法是 HashMap 最核心的逻辑,我们顺着源码梳理它的执行链路:

java 复制代码
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

在 putVal 方法中,主要做了以下几件事:

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

  2. 桶为空:直接新建 Node 放入。

  3. 桶不为空:

    • 如果第一个节点就命中(key 相同),直接更新 value。

    • 如果是红黑树节点,调用红黑树的插入方法。

    • 如果是链表,遍历链表。如果找到相同的 key 则更新;如果没找到,在尾部插入新节点。插入后如果链表长度 >= 8,触发树化逻辑(treeifyBin)。

  4. 扩容检查 :如果 size > threshold,调用 resize()。


3. 扩容机制:resize()

扩容是 HashMap 中最消耗性能的操作。JDK 8 对扩容进行了巧妙的优化:

java 复制代码
final Node<K,V>[] resize() {
    // 1. 计算新容量和新阈值(通常是旧的两倍)
    // 2. 创建新数组
    // 3. 数据迁移
}

JDK 8 扩容的精髓:高位迁移

在 JDK 7 中,扩容时需要重新计算每个元素的 hash % length。而在 JDK 8 中,因为容量永远是 2 的幂,扩容后元素的新位置要么在原位置,要么在原位置 + 旧容量。

java 复制代码
// 判断原哈希值新增参与运算的位是 0 还是 1
if ((e.hash & oldCap) == 0) {
    // 低位,留在原索引
} else {
    // 高位,移动到 原索引 + oldCap
}

这种位运算的优化,让扩容效率提升了数倍。


4. 获取元素:get(Object key)

get 的逻辑相对简单:

  1. 计算 hash。

  2. 定位到数组下标。

  3. 如果第一个节点命中,直接返回。

  4. 如果是红黑树,调用 getTreeNode 查找。

  5. 如果是链表,遍历查找。


四、 面试常问的"坑点"

1. HashMap 是线程安全的吗?

绝对不是。​ 在多线程环境下,JDK 8 的 HashMap 可能会出现死循环 (虽然 JDK 8 修复了 JDK 7 头插法导致的死循环,但数据覆盖问题依然存在)。高并发请使用 ConcurrentHashMap。

2. 为什么 String、Integer 适合作为 Key?

因为它们是不可变类(Immutable) 。如果 Key 是可变的,对象被放入 Map 后,其 hashCode 发生变化,就再也找不到原来的值了。

3. 初始容量设为 1000,实际是多少?

HashMap 会将其调整为大于等于该值的最小的 2 的幂 ,即 1024。


五、 总结

JDK 8 的 HashMap 是一次非常优秀的架构升级:

  1. 数据结构:数组 + 链表 + 红黑树,兼顾了性能和内存。

  2. 哈希算法:扰动函数减少冲突。

  3. 扩容机制:高位运算优化迁移效率。

  4. 树化策略:基于泊松分布的阈值设定,既保证了极端情况下的性能,又避免了不必要的结构转换。

理解 HashMap 的源码,不仅能帮你从容应对面试,更能让你在设计高并发、高性能系统时,对"哈希"这一基础数据结构有更深刻的敬畏和运用。


写在最后

以上就是这次关于HashMap底层源码解析的学习记录。

如果这篇笔记对你有帮助,点个赞鼓励一下吧~ 有任何疑问也欢迎在评论区一起讨论交流!

相关推荐
憧憬成为java架构高手的小白1 小时前
LangGraph4j 实战(下)
java
天诰1 小时前
SA-K-11 检索会消失吗与四个未解问题
java·服务器·人工智能·python·机器学习·ai编程
这个昵称不容易5 小时前
SpringBoot「衣依」服装销售平台:144个Java文件180个接口,从挑衣服到收货评价整条链路都在
java·springboot·毕设
计算机毕设定制辅导-无忧学长5 小时前
《基于微信小程序的居家医疗陪护预约系统设计与实现》
java·spring boot·微信小程序·uniapp·居家医疗陪护预约系统
文丑颜不良啊7 小时前
LeetCode2221~2240题目解析
java·leetcode
秋饼7 小时前
Computer Use Agent Java 落地实战:感知-行动循环、执行器与安全围栏生产级实现
java·ai·技术分享·后端开发
文丑颜不良啊9 小时前
LeetCode2161~2180题目解析
java·leetcode
文丑颜不良啊10 小时前
LeetCode2141~2160题目解析
java·leetcode
LabVIEW开发10 小时前
LabVIEW 连接板:28 个端子怎么排,优先级怎么定
java·算法·labview
天天被压力11 小时前
【A股分钟级高频数据完全手册 #07】北交所历史分钟K线接口:京市Pro版分时批量
java·人工智能·python