当前位置: 代码网 > it编程>编程语言>Java > Java-HashMap底层原理和扩容机制详解

Java-HashMap底层原理和扩容机制详解

2026年09月11日 Java 我要评论
一句话总结hashmap底层采用数组+链表/红黑树结构,通过哈希算法确定元素存储位置。默认初始容量16,负载因子0.75,当元素数量超过(容量×负载因子)时触发扩容。扩容时创建双倍容量新数

一句话总结

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. 数据丢失:多线程扩容时,可能丢失部分键值对(如两个线程同时计算出相同的新桶位置,导致其中一个线程的数据被覆盖)。

总结

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com