首页 > 编程开发 > Java    日期:2026-09-12 / 浏览:次

一句话总结

HashMap底层采用数组+链表/红黑树结构,通过哈希算法确定元素存储位置。默认初始容量16,负载因子0.75,当元素数量超过(容量×负载因子)时触发扩容。

扩容时创建双倍容量新数组,通过高位运算重新计算节点位置(JDK8优化为无需重新hash),原数据通过尾插法迁移到新数组。

链表长度超过8且数组长度≥64时会转为红黑树,提升查询效率。

详细解析

一、底层数据结构:数组 + 链表(JDK7) → 数组 + 链表/红黑树(JDK8)

HashMap 的底层是一个 哈希表(Hash Table),本质是一个动态扩容的数组(称为table),数组的每个元素是一个 链表(或 红黑树,JDK8 及以后),用于存储哈希冲突的键值对。

核心结构术语

  • 桶(Bucket):数组的每个槽位(table[i])称为一个桶,用于存放哈希值相同的键值对。
  • 哈希冲突(Hash Collision):不同键通过哈希函数计算出相同的桶下标,导致多个键值对需要存放在同一个桶中。
  • 链表(Entry 或 Node 节点):JDK7 中桶内元素以链表形式存储(节点类型为Entry<K,V>);JDK8 中改为Node<K,V>,当链表长度超过阈值时转换为红黑树。
  • 红黑树(TreeNode):JDK8 引入,当链表长度 ≥8 且数组长度 ≥64 时,链表转换为红黑树(节点类型为TreeNode<K,V>),以将查找时间复杂度从 O(n) 优化到 O(logn)。

二、核心机制:哈希计算、冲突解决、扩容

哈希计算与桶定位

HashMap 通过以下步骤确定键值对的存储位置:

步骤 1:计算键的哈希值

键的哈希值通过key.hashCode()方法获取,但 HashMap 会对其进行二次哈希(hash()方法),目的是 减少哈希冲突。

JDK7 的hash()方法通过多次位运算(如异或、右移)扩散哈希值;

JDK8 简化为:(h = key.hashCode()) ^ (h >>> 16)(高 16 位与低 16 位异或),让高位参与低位计算,减少低位冲突。

步骤 2:确定桶下标

桶下标通过(n - 1) & hash计算(n是数组长度,必须是 2 的幂次)。该计算等价于hash % n,但位运算更高效。数组长度为 2 的幂次是为了保证(n-1) & hash的结果均匀分布。

若n非 2 的幂次,&运算会导致某些桶永远无法被访问。

比如n=10,那么n-1=9,二进制为1001。(n-1) & hash 等价于取 hash 的二进制与 1001 的按位与,结果的二进制只能是以下四种可能(因为 1001 只有第 0 位和第 3 位是 1):0000(0)、0001(1)、1000(8)、1001(9)。因此,无论 hash 是什么值,最终的桶下标只能是 0、1、8、9 这四个值。

哈希冲突解决:链地址法

当不同键的哈希值映射到同一个桶时,HashMap 使用 链地址法 解决冲突:将冲突的键值对以链表形式挂在同一个桶下。

JDK8 之前链表的插入方式是 头插法(新节点插入链表头部),JDK8 改为 尾插法(新节点插入链表尾部),避免扩容时的死循环问题(下文详述)。

扩容机制:动态调整数组大小

HashMap 通过 扩容(resize) 保持负载因子(Load Factor)在合理范围,避免哈希冲突过多导致性能下降。

触发条件:

  • 当元素数量(size)超过容量(capacity) × 负载因子(loadFactor)时触发扩容。
  • 默认容量为 16,负载因子为 0.75(空间与时间的权衡:负载因子过小会导致频繁扩容;过大则哈希冲突概率增加)。

扩容过程:

JDK8 扩容优化:

由于数组长度是 2 的幂次,旧数组的桶下标为i,新数组长度为2×oldCap,新桶下标只能是i或i + oldCap(通过hash & oldCap是否为 0 判断)。因此,无需重新计算哈希值,只需判断hash的最高位(相对于旧容量的位置)是否为 0,即可确定新位置,大幅提升扩容效率,可以看下图容量从16扩充为32的resize示意图加深理解。

  • 创建新数组:容量翻倍(newCap = oldCap << 1),新数组长度仍为 2 的幂次。
  • 迁移元素:将旧数组中的所有键值对重新分配到新数组的桶中(JDK7 需重新计算哈希值,JDK8 优化为通过hash & oldCap判断是否需要移动)。

三、JDK7 与 JDK8 的核心差异

特性 JDK7 实现 JDK8 实现
底层结构 数组 + 链表(Entry 节点) 数组 + 链表/红黑树(Node/TreeNode 节点)
冲突解决 链表(长度无限制,查找 O(n)) 链表长度 ≥8 且数组长度 ≥64 时转为红黑树(查找 O(logn))
插入方式 头插法(新节点插入链表头部) 尾插法(新节点插入链表尾部)
扩容时哈希计算 重新计算哈希值(hash()方法) 通过hash & oldCap判断是否需要移动(无需重新计算哈希)
死循环问题 多线程扩容时可能导致链表成环(死循环) 尾插法避免了死循环,但仍存在数据覆盖问题(线程不安全本质未变)

四、线程不安全的原因

HashMap 是 线程不安全 的,多线程环境下可能导致以下问题:

  1. 数据覆盖(最常见):多个线程同时执行put操作时,若哈希冲突导致两个线程同时修改同一个桶的链表,可能出现后写入的数据覆盖先写入的数据。
  2. 扩容死循环(JDK7):JDK7 采用头插法扩容,多线程迁移元素时可能导致链表成环(如线程 A 和 B 同时迁移同一链表,指针互相引用),后续查询时陷入死循环。
  3. 数据丢失:多线程扩容时,可能丢失部分键值对(如两个线程同时计算出相同的新桶位置,导致其中一个线程的数据被覆盖)。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章