当前位置: 首页 > news >正文

HashMap(JDK1.7到1.8的过渡)

HashMap

HashMap 是 Java 中常用的一种键值对存储结构,它基于哈希表实现,基本结构为数组+链表数组+JDK1.8引入的红黑树

Put()方法的原理

1. 根据Key计算哈希值

当调用put(key, value)方法时,首先会根据key计算其哈希值。在 Java 中,key对象的hashCode()方法会被调用,返回一个int类型的哈希码。例如:

HashMap<String, Integer> map = new HashMap<>();String key = "example";int hashCode = key.hashCode();

这个哈希码会被进一步处理,以减少哈希冲突的可能性。在 JDK 8 及以后的版本中,会对hashCode进行扰动处理(hash方法) ,让哈希分布更加均匀。

1.1二次Hash的目的

在 JDK 1.8 中,HashMap 的put方法在计算哈希值时, “二次哈希”(哈希扰动)的目的是为了减少哈希冲突 、让哈希值分布更均匀,从而提升存储效率。

1.2二次Hash的具体实现

前置信息:HashMap 涉及的 Hash 值基于int类型,因此二进制位数为 32 位(包括符号位)

static final int hash(Object key) {int h;// key为null时哈希值为0;否则对key的hashCode进行“扰动”return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
  • keynull,直接返回哈希值0

  • key不为null,先获取key原始 hashCode(记为h),再将h无符号右移 16 位(把高 16 位移到低 16 位),最后与原始的h进行异或运算^),得到最终用于计算下标的哈希值。

2. 通过Hash确定数组下标

通过哈希值计算出在table数组(哈希表)中的下标。具体是将哈希值与数组长度减 1 进行按位与运算((n - 1) & hash,其中n是数组长度),从而得到一个在数组范围内的索引值。例如,数组长度为 16(n = 16),哈希值为5,则计算出的下标为 (16 - 1) & 5 = 5

int index = (table.length - 1) & hash;
2.1二次Hash解决 “高位信息浪费”,减少哈希冲突

HashMap 的数组容量(table.length)通常是2 的 n 次幂(如默认初始容量 16,二进制为10000)。计算数组下标时,用的是(n - 1) & hash(位与运算,效率比取模高)

思考:当数组容量较小时(比如 16),n - 115(二进制01111),此时只有 hashCode 的 “低 4 位” 参与了下标计算,高 16 位的信息被 “浪费” 了 —— 如果不同key低 n 位相同,即使高 16 位不同,也会映射到同一个数组下标,导致哈希冲突。例如:

  • Key1hashCode() 返回值为 0b1010110010110011 00001010(二进制,共 32 位。高 16 位为 1010110010110011,低 4 位为 1010);

  • Key2hashCode() 返回值为 0b1100101011001011 00001010(二进制,高 16 位为 1100101011001011,低 4 位同样为 1010)。

  • 低四位相同,高16位不相同,如果没有Hash扰动,他们将出现Hash冲突,之后就需要调用equals()比对,影响效率。

解决方案:二次Hash-----通过 “二次哈希”(h ^ (h >>> 16)),可以让高 16 位的特征与低 16 位的特征混合,让更多位的信息参与哈希计算,从而让最终的哈希值分布更分散,降低冲突概率。

3. 检查数组位置是否为空

获取到数组下标后,检查table[index]对应的位置是否为空:

  • 如果为空:直接在该位置创建一个新的Node节点(在 JDK 8 中,NodeHashMap中存储键值对的基本结构),将键值对存储在这个节点中。

  • 如果不为空:说明该位置已经存在元素,发生了哈希冲突,需要进一步处理。

4. 处理哈希冲突

当发生哈希冲突时,JDK 8 之前采用链表法解决冲突,即新的节点会被添加到链表的头部;JDK 8 及以后,引入了红黑树来优化处理冲突:

  • 检查key是否相同:遍历该位置的链表(或红黑树),逐个比较节点的key。如果找到key相同的节点,则直接用新的value覆盖旧的value

  • 如果key不同:

    • 链表情况:如果当前结构是链表,且没有达到树化阈值(默认链表长度达到 8,且数组长度大于等于 64 时会进行树化),则将新节点添加到链表的尾部。

    • 树化情况:如果链表长度达到树化阈值,并且数组长度也满足条件,会将链表转换为红黑树,然后将新节点按照红黑树的插入规则插入到树中。

5. 检查容量并扩容

在插入新节点后,会检查当前 HashMap 的容量是否超过了负载因子(默认是 0.75)与数组长度的乘积(Entry数组容量的75%)。如果超过了,就会触发扩容操作,调用resize()方法。扩容会创建一个新的更大的数组(通常是原来的 2 倍),然后将原数组中的元素重新哈希并复制到新数组中。

JDK1.8HashMap了加入红黑树

JDK 1.8 对 HashMap 的核心优化之一是引入红黑树结构处理哈希冲突,解决了 JDK 1.7 中链表过长导致的性能退化问题。

1.JDK 1.7 的链表性能瓶颈

JDK 1.7 中,HashMap 仅用单链表处理哈希冲突:当多个键哈希冲突时,所有冲突元素会被串联成一条链表,存储在同一数组下标(桶)中。

  • 当哈希冲突严重(如大量键映射到同一桶),链表会变得很长(例如长度为 n)。

  • 此时无论是查询(get)还是插入(put),都需要线性遍历链表,时间复杂度从O(1)退化为 O(n),性能急剧下降。

2.JDK 1.8 的红黑树优化

JDK 1.8 引入红黑树(一种自平衡二叉查找树),当链表长度达到阈值时(默认是:链表长度大于8,且数组长度大于64),自动将链表转换为红黑树。

注:如果数组容量不足 64,即使链表长度超过 8,也不会进行树化,但是会先触发扩容(而非树化),避免在小容量数组上过早树化(因数组扩容后冲突有可能缓解)。

2.1链表退化

当红黑树中的元素数量减少到 6UNTREEIFY_THRESHOLD = 6)时,会自动退化为链表,避免红黑树在元素较少时的维护成本(红黑树的平衡操作比链表复杂)。

红黑树的优势是 “元素多时有 O(logn) 性能”,但维护成本(平衡操作、节点存储) 比链表高。当元素数量很少时(比如≤6),红黑树的维护成本已经超过了 “链表遍历 O(n)” 的成本 —— 此时用链表更高效。

3.性能变化

红黑树的查找、插入、删除操作时间复杂度均为 O(log n)n 为树中元素数),相比链表的 O (n) 有显著提升:

  • 例如,当冲突元素达到 100 个时,链表需要遍历 100 次,而红黑树仅需约 7 次(log2(100) ≈ 7)。

jdk1.7到jdk1.8插入元素方法的更改

1.JDK1.7的头插法扩容

HashMap 扩容时,需要创建新数组,并将旧数组中的元素(包括链表节点)迁移到新数组。

  • 迁移链表节点时,JDK 1.7 采用 「头插法」:新节点会被插入到链表头部,原链表的后续节点依次 “后移”。

  • 头插法的效果:会反转链表的顺序(比如旧链表是 A→B→C,头插后会变成 C→B→A)。

注:它头插法指的是:从旧的HashMap中的某个链表中从头节点开始,没遍历一个节点就将该节点头插到新HashMap对应位置对应链表的头部,所以会造成链表元素位置调换。

1.1多线程下构成“循环链表”

假设两个线程 T1T2 同时对同一个 HashMap 进行扩容,且旧数组中有一条链表 A→BA 是头节点,B 是后续节点)。

步骤 1:线程 T1、T2 读取旧链表结构

两个线程都 “看到” 旧链表的结构是 A → BA.next = BB.next = null)。

步骤 2:线程 T1 先执行扩容(头插法迁移)

T1 开始迁移链表:

  • 先处理节点 A:将 A 插入新数组的对应位置,此时新链表的头是 AA.next = null)。

  • 再处理节点 B头插法B 插入到新链表头部,此时 B.next = A,新链表变成 B → A(链表顺序反转)。

  • T1 完成扩容,链表顺序从 A→B 变为 B→A

步骤 3:线程 T2 被挂起后恢复,基于 “旧视图” 操作

T2 在 T1 扩容过程中被挂起(比如时间片用完)。当 T2 恢复时,并不知道 T1 已经修改了链表顺序,仍然认为旧链表是 A → B

T2 开始迁移链表:

  • 先处理节点 A:将 A 插入新数组,A.next = null(此时新链表头是 A)。

  • 再处理节点 B头插法试图将 B 插入头部,执行 B.next = A

步骤 4:循环链表形成

此时,T1 已经将 B.next 设为 A(步骤 2),而 T2 又执行 B.next = A(步骤 3)。最终可能导致:

  • A.next 指向 B(T2 处理 A 时的旧逻辑残留,或 T1 操作后的意外影响);

  • B.next 指向 A(T2 头插 B 时的操作)。

于是,ABnext 指针形成循环引用A → B → A → B → …)。后续若有线程遍历这条链表,会陷入无限循环,导致 CPU 占用 100%。

2.JDk1.8的尾插法扩容

JDK 1.8 扩容时,链表迁移改用 「尾插法」:新节点被追加到链表尾部,链表顺序保持不变(旧链表 A→B,迁移后还是 A→B)。

因此,即使多线程扩容(虽然 HashMap 仍非线程安全,但尾插法的 “顺序不变” 特性),也不会出现 “链表逆序 + 并发修改导致循环” 的问题,从根源上消除了循环链表的风险。

jdk1.7和jdk1.8对比对HashMap做了哪些处理?

  1. 加入了红黑树,使最坏情况下查询效率从o(n)-->o(logn)
  2. 扰动算法(二次Hash):优化Hash值计算 (h=key.hashCode())^(h>>>16),减少hash冲突。
  3. 插入元素方式由头插法变成尾插法

    • JDK1.7:采用头插法,扩容后链表节点顺序会反转。多线程并发扩容时,易因 “线程切换 + 头插反转” 导致循环链表(死循环风险)。

    • JDK1.8:采用尾插法,扩容后链表节点顺序保持不变。从根源上避免了 “头插反转 + 多线程并发” 导致的循环链表问题(尽管 HashMap 仍非线程安全,但此设计消除了死循环场景)。

  4. 优化resize():

    • JDK1.7:扩容时需为所有元素重新计算桶下标(因数组容量改变,哈希取模的规则随之变化),计算开销大。

    • JDK1.8:利用 (e.hash & oldCap) == 0 快速判断新位置:

      • 若结果为true,元素在新表中的位置与旧表完全相同

      • 若结果为false,元素在新表中的位置为 旧位置 + 旧容量。 无需重新计算哈希,仅通过位运算即可快速定位,大幅提升扩容效率。


文章转载自:

http://JUQFbe56.ycpnm.cn
http://oNLmQmrk.ycpnm.cn
http://58MXpsJg.ycpnm.cn
http://PIw7eT5v.ycpnm.cn
http://Nb2UMRWw.ycpnm.cn
http://uJOSaCs1.ycpnm.cn
http://Tx5tw4cK.ycpnm.cn
http://GAXiaT6Z.ycpnm.cn
http://jYAs83ZS.ycpnm.cn
http://5b1oeeoM.ycpnm.cn
http://ORk5KduT.ycpnm.cn
http://WZAoiISM.ycpnm.cn
http://cJN9yZeG.ycpnm.cn
http://bPO6EntQ.ycpnm.cn
http://FNvcT0ny.ycpnm.cn
http://2X0KUB0h.ycpnm.cn
http://kIguUuDK.ycpnm.cn
http://mnJewnNI.ycpnm.cn
http://6GWUDzJB.ycpnm.cn
http://0BVlZBLw.ycpnm.cn
http://O1b7HfcO.ycpnm.cn
http://QRyF1D8V.ycpnm.cn
http://qu4rS9Zh.ycpnm.cn
http://d0hNmCUK.ycpnm.cn
http://ZgWbTMAn.ycpnm.cn
http://BPj2HjmX.ycpnm.cn
http://R9Y3Sb8z.ycpnm.cn
http://sWu3eZgZ.ycpnm.cn
http://uwnIbxc1.ycpnm.cn
http://FFEHCJ9k.ycpnm.cn
http://www.dtcms.com/a/375218.html

相关文章:

  • 趣味学RUST基础篇(函数式编程迭代器)
  • 抗ASIC、抗GPU 的密码哈希算法(安全密钥派生)Argon2算法
  • Nginx 实战系列(六)—— Nginx 性能优化与防盗链配置指南
  • 深入解析 Apache Flink Checkpoint 与 Savepoint 原理与最佳实践
  • C#WPF控制USB摄像头参数:曝光、白平衡等高级设置完全指南
  • 第2节-过滤表中的行-IN
  • 2025年渗透测试面试题总结-60(题目+回答)
  • 【GD32】ROM Bootloader、自定义Bootloader区别
  • 业务用例和系统用例
  • Google AI Mode 颠覆传统搜索方式,它是有很大可能的
  • MTC出席SAP大消费峰会:行业深度×全球广度×AI创新,助力韧性增长
  • 彩笔运维勇闯机器学习--决策树
  • 成都金牛区哪里租好办公室?国际数字影像产业园享税收优惠
  • vue3 实现将页面生成 pdf 导出(html2Canvas + jspdf)
  • golang 面试常考题
  • 单例模式(C++)
  • All in AI之二:数学体系的建立
  • 【Python】S1 基础篇 P5 字典模块指南
  • MySQL底层架构设计原理详细介绍
  • 《ServiceMesh落地避坑指南:从智慧园区故障看Envoy配置治理》
  • 【ARMv7-M】复位向量与启动过程
  • SQL面试题及详细答案150道(136-150) --- 性能优化与数据库设计篇
  • CMake Qt程序打包与添加图标详细教程
  • 【MySQL】mysql-connector-cpp使用
  • Oracle RAC认证矩阵:规避风险的关键指南
  • CTF-Web手的百宝箱
  • Django高效查询:values_list实战详解
  • Redis核心数据结构
  • 海外代理IP平台Top3评测:LoongProxy、神龙动态IP、IPIPGO哪家更适合你?
  • 开发避坑指南(43):idea2025.1.3版本启动springboot服务输入jvm参数解决办法