【集合】JDK1.8 HashMap 底层数据结构深度解析

一、核心数据结构:为什么是 "数组 + 链表 + 红黑树"?​

HashMap 的底层设计本质是用空间换时间,通过哈希表的快速定位特性,结合链表和红黑树处理冲突,平衡查询与插入效率。​

1.1 基础容器:哈希桶数组(Node [] table)​

  • 数组角色:作为哈希表的 "骨架",每个下标对应一个哈希桶(bucket),通过哈希值直接定位元素所在桶的位置,实现 O (1) 级别的快速访问。​
  • Node 节点结构:数组中存储的是Node<K,V>对象,包含四个核心字段:
static class Node<K,V> implements Map.Entry<K,V> {final int hash;    // 键的哈希值(经过扰动处理)final K key;       // 键(唯一)V value;           // 值Node<K,V> next;    // 下一个节点引用(用于链表)
}

 

这里的next字段是实现链表的关键,当多个元素哈希冲突时,会通过该字段串成链表。​

1.2 解决冲突:链表的作用​

当两个不同的 key 计算出相同的哈希值(即哈希冲突)时,HashMap 会将新元素以链表节点的形式 "挂" 在同一哈希桶的尾部。这种处理方式称为链地址法,是哈希表解决冲突的经典方案。​

1.3 性能优化:红黑树的引入​

JDK1.8 之前,HashMap 仅用数组 + 链表的结构,当哈希冲突严重时(如大量元素集中在同一桶中),链表会退化为 "长链",此时查询时间复杂度会从 O (1) 退化到 O (n)。​

为解决这个问题,JDK1.8 引入了红黑树(TreeNode):当链表长度超过阈值(默认 8)且数组长度≥64 时,链表会自动转为红黑树,将查询时间复杂度优化到 O (log n)。​

二、关键机制解析:哈希、扩容与树化​

2.1 哈希函数:如何计算元素在数组中的位置?​

HashMap 的高效查询依赖于哈希值的精准计算,核心步骤分为两步:​

① 计算 key 的哈希值(扰动函数)

static final int hash(Object key) {int h;// 1. 取key的hashCode值;2. 高16位与低16位异或(扰动处理)return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 为什么要做扰动处理?​

若直接使用key.hashCode()的低几位计算索引,当哈希值的高位变化大但低位变化小时,容易导致哈希冲突(例如hashCode=0x0001FFFF和0x0002FFFF,低 16 位相同)。通过高 16 位与低 16 位异或,能让高位信息参与哈希计算,减少冲突概率。​

② 计算数组索引

// n为数组长度(必须是2的幂)
int index = (n - 1) & hash;
  • 这里用(n-1) & hash代替取模运算(hash % n),因为当 n 是 2 的幂时,两者结果等价,但位运算效率更高。​

2.2 扩容机制:数组不够用了怎么办?​

当 HashMap 中的元素数量超过阈值(threshold = 容量 × 负载因子)时,会触发扩容(resize),将数组长度翻倍(新容量 = 旧容量 ×2)。​

扩容的核心步骤:​

        (1)计算新容量与新阈值:默认初始容量为 16,负载因子 0.75,初始阈值 = 16×0.75=12。扩容后新容量 = 32,新阈值 = 32×0.75=24。​

        (2)创建新数组:长度为新容量。​

        (3)迁移旧元素:将旧数组中的元素重新计算索引,迁移到新数组中(JDK1.8 优化点:通过hash & oldCap判断元素是否需要移动,避免重复计算哈希)。​

扩容时的元素迁移逻辑:​

  • 若元素所在桶是单节点:直接计算新索引并放入新数组。​
  • 若元素所在桶是链表:​

通过hash & oldCap将链表拆分为两个子链表:​

  • 结果为 0:元素留在原索引位置(新数组的j处)。​
  • 结果为非 0:元素迁移到新索引位置(新数组的j + oldCap处)。​
  • 若元素所在桶是红黑树:拆分树节点为两个子树,若子树长度≤6 则退化为链表。​

2.3 树化与反树化:链表和红黑树如何转换?​

树化(链表转红黑树)的触发条件:​

  1. 链表长度≥8(TREEIFY_THRESHOLD = 8)。​
  1. 数组长度≥64(MIN_TREEIFY_CAPACITY = 64)。​

若数组长度不足 64,会先触发扩容而非树化,避免在小容量数组上过度树化浪费资源。​

反树化(红黑树转链表)的触发条件:​

当红黑树的节点数量≤6(UNTREEIFY_THRESHOLD = 6)时,会自动退化为链表,减少树结构带来的维护成本。​

为什么阈值是 8 和 6?​

  • 官方注释提到,根据泊松分布,链表长度达到 8 的概率仅为 0.00000006(几乎是小概率事件),此时树化收益大于成本。​
  • 用 6 作为反树化阈值,是为了避免链表在 8 附近频繁树化 / 反树化( hysteresis 机制)。​

三、源码直击:put () 与 resize () 核心逻辑​

3.1 put () 方法:元素插入全流程

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 步骤1:数组为空则初始化(第一次put时触发)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 步骤2:计算索引,若桶为空则直接插入新节点if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;// 步骤3:若桶中首个节点的key与插入key相同,直接覆盖if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 步骤4:若桶中是红黑树,则调用树插入方法else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 步骤5:若桶中是链表,遍历链表寻找插入位置else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 链表长度≥8,触发树化if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}// 找到相同key的节点,跳出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 步骤6:若存在相同key的节点,替换valueif (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 步骤7:元素数量超过阈值,触发扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

3.2 resize () 方法:扩容核心逻辑

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 计算新容量和新阈值(省略细节)if (oldCap > 0) {newCap = oldCap << 1; // 容量翻倍newThr = oldThr << 1; // 阈值翻倍} else if (oldThr > 0) {newCap = oldThr;} else {newCap = 16; // 默认初始容量newThr = 12; // 默认初始阈值(16*0.75)}// 创建新数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 迁移旧元素到新数组(省略链表和红黑树的迁移细节)if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 单节点直接迁移if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 链表和红黑树的迁移逻辑(见上文)// ...}}}return newTab;
}

四、高频面试题:HashMap 的那些 "为什么"​

4.1 为什么 HashMap 的容量必须是 2 的幂?​

  • 确保(n-1) & hash等价于hash % n,用高效的位运算替代取模。​
  • 扩容时可通过hash & oldCap快速判断元素是否需要迁移(结果为 0 则留在原位置,否则迁移到j+oldCap)。​

4.2 为什么选择红黑树而非 AVL 树?​

  • 红黑树:插入和删除时最多需要 2 次旋转,调整成本低,适合频繁插入删除的场景。​
  • AVL 树:是严格平衡树,查询效率更高,但插入删除可能需要多次旋转,调整成本高。​

HashMap 更注重整体的插入删除效率,因此选择红黑树。​

4.3 HashMap 为什么线程不安全?如何解决?​

  • 线程不安全表现:多线程并发扩容时,JDK1.7 中可能出现链表环(导致死循环);JDK1.8 中虽解决了环问题,但仍可能出现数据覆盖。​
  • 线程安全方案:​
  • 使用ConcurrentHashMap(JDK1.7 分段锁,JDK1.8 CAS+ synchronized)。​
  • 通过Collections.synchronizedMap(new HashMap<>())包装(性能较差,不推荐)。​

4.4 负载因子为什么默认是 0.75?​

  • 负载因子过小(如 0.5):哈希冲突少,但数组扩容频繁,浪费空间。​
  • 负载因子过大(如 1.0):空间利用率高,但哈希冲突概率剧增,查询效率下降。​

0.75 是时间与空间成本的平衡点,由泊松分布计算得出,此时哈希冲突概率较低。​

五、总结​

JDK1.8 的 HashMap 通过数组 + 链表 + 红黑树的复合结构,结合哈希函数的扰动优化、高效扩容机制和树化策略,实现了 "查询快、插入快、冲突少" 的核心目标。其设计细节(如 2 的幂容量、0.75 负载因子、8/6 树化阈值)处处体现了 "平衡取舍" 的设计思想。​

理解这些底层原理,不仅能帮助我们在开发中规避 HashMap 的使用陷阱(如 key 未重写 hashCode/equals 导致的内存泄漏),更能在面试中脱颖而出。建议结合源码调试,观察哈希冲突、扩容、树化的实际过程,加深理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93131.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93131.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【element-ui】HTML引入本地文件出现font找不到/fonts/element-icons.woff

文章目录目录结构问题复现解决办法目录结构 |-web|- public|- lib|- ...|- index.htmlindex.html <!DOCTYPE html> <html> <head><meta charset"UTF-8"><!-- import CSS --><link rel"stylesheet" href"./public/…

Windows|CUDA和cuDNN下载和安装,默认安装在C盘和不安装在C盘的两种方法

本篇文章将详细介绍在Windows操作系统中配置CUDA和cuDNN的步骤。通过本教程&#xff0c;您将能够轻松完成CUDA和cuDNN的安装、环境变量配置以及与深度学习框架&#xff08;如TensorFlow和PyTorch&#xff09;兼容性测试&#xff0c;从而为您的深度学习项目提供强大的硬件支持。…

Vue 项目动态接口获取翻译数据实现方案(前端处理语言翻译 vue-i18n)

在大型多语言项目中&#xff0c;将翻译数据硬编码在项目中往往不够灵活。通过接口动态获取翻译数据&#xff0c;并结合本地缓存提升性能&#xff0c;是更优的国际化实现方式。本文将详细介绍如何在 Vue 项目中实现这一方案。 方案优势 灵活性高&#xff1a;翻译内容更新无需修改…

Mybatis-plus多数据源

适用于多种场景&#xff1a;纯粹多库、 读写分离、 一主多从、 混合模式等目前我们就来模拟一个纯粹多库的一个场景&#xff0c;其他场景类似场景说明&#xff1a;我们创建两个库&#xff0c;分别为&#xff1a; mybatis_plus&#xff08;以前的库不动&#xff09;与my…

广东省省考备考(第五十六天7.25)——常识:科技常识(听课后强化训练)

错题解析解析解析解析解析解析解析解析解析标记题解析解析今日题目正确率&#xff1a;40%

RabbitMQ简述

RabbitMQ简述 RabbitMQ 是一个开源的 消息代理&#xff08;Message Broker&#xff09; 软件&#xff0c;实现了 高级消息队列协议&#xff08;AMQP&#xff09;&#xff0c;用于在分布式系统中存储、转发消息&#xff0c;支持异步通信、解耦服务、负载均衡和消息缓冲。 核心…

skywalking应用性能监控

1.skywalking描述 官方文档 SkyWalking 是一个开源的可观测性平台&#xff0c;用于收集、分析、汇总和可视化来自服务及云原生基础设施的数据。SkyWalking 为维护分布式系统的清晰视图提供了简便的方法&#xff0c;即使是在跨云环境中也能做到。它是一款专为云原生、基于容器的…

如何彻底清除服务器上的恶意软件与后门

清除服务器上的恶意软件与后门 是确保服务器安全的关键步骤。恶意软件和后门可能导致数据泄露、性能下降&#xff0c;甚至服务器被攻击者完全控制。以下是彻底清除恶意软件与后门的详细指南&#xff0c;包括 检测、清理、修复与预防 的步骤。1. 彻底清除恶意软件与后门的步骤1.…

Linux和Windows基于V4L2和TCP的QT监控

最近工作需要用QT做一个网络摄像头测试&#xff0c;简单记录&#xff1a;服务端&#xff0c;主机配置为Ubuntu&#xff0c;通过端口12345采集传输MJPEG格式图片windows客户端&#xff0c;QT Creator通过ip地址连接访问提前准备服务端需要安装QT5sudo apt-get install qt5-defau…

yolo格式

labelimg中的格式yolo格式id 框中心点X对于总图片的比例 框中心点Y对于总图片的比例 框X总长度对于总图片的比例 框Y总长度对于总图片的比例

Day 8-zhou R包批量安装小补充!!!

BiocManager::install(c(“S4Vectors”, “BiocGenerics”)) 以下是使用BiocManager安装S4Vectors和BiocGenerics包的详细步骤。这些步骤基于最新的Bioconductor和R版本&#xff08;R 4.5&#xff09;。 安装步骤安装BiocManager 如果你还没有安装BiocManager&#xff0c;可以使…

电商项目_核心业务_数据归档

无论采用哪种存储系统&#xff0c;数据查询的耗时取决于两个因素查找的时间复杂度数据总量查找的时间复杂度又取决于查找算法数据存储结构以Mysql存储的订单数据为例&#xff0c;随着业务的发展&#xff0c;数据量越来越大&#xff0c;对一些历史归档数据的查询&#xff0c;如果…

第十讲:stack、queue、priority_queue以及deque

目录 1、stack 1.1、stack的使用 1.2、stack的OJ题 1.2.1、最小栈 1.2.2、栈的压入弹出序列 1.2.3、逆波兰表达式求值 1.3、stack的模拟实现 2、queue 2.1、queue的使用 2.2、queue的OJ题 2.2.1、二叉树的层序遍历 2.3、queue的模拟实现 3、priority_queue 3.1、…

如何思考一个动态规划问题需要几个状态?

如何思考一个动态规划问题需要几个状态&#xff1f;第一步&#xff1a;思考 角色第二步&#xff1a;考虑 过去的影响第三步&#xff1a;画出状态转移图第四步&#xff1a;写出状态转移方程第五步&#xff1a;验证是否能覆盖所有路径 边界几个常见题目总结&#xff1a;第一步&a…

【每天一个知识点】生成对抗聚类(Generative Adversarial Clustering, GAC)

&#x1f4d8; 生成对抗聚类&#xff08;Generative Adversarial Clustering, GAC&#xff09; 一、研究背景与动机 聚类是无监督学习中的核心任务。传统方法如 K-means、GMM、DBSCAN 等难以适应高维、非线性、复杂结构数据。 生成对抗聚类&#xff08;GAC&#xff09; 融合…

Qt 窗口 工具栏QToolBar、状态栏StatusBar

每日激励&#xff1a;“不设限和自我肯定的心态&#xff1a;I can do all things。 — Stephen Curry” 绪论​&#xff1a; 一段时间没有更新&#xff0c;这段时间一直在忙各种事情&#xff0c;后续将再次上路持续更新C相关知识 本章将继续前面的QT篇章&#xff0c;本章主要讲…

FFmpeg——参数详解

FFmpeg参数详解一、基本命令结构1.1、查询参数1.1.1、version1.1.2、buildconf1.1.3、devices1.1.4、formats1.1.5、muxers1.1.6、demuxers1.1.7、codecs1.1.8、decoders1.1.9、encoders1.1.10、bsfs1.1.11、protocols1.1.12、filters1.1.13、pix_fmts1.1.14、layouts1.1.15、s…

流媒体传输:RTSP传输详解(包含RTP,RTCP,RTSP详解)

一、什么是 RTSP​协议 1.1 RTSP 协议简介​ RTSP&#xff0c;全称实时流传输协议&#xff08;Real Time Streaming Protocol&#xff09;&#xff0c;是一种位于应用层的网络协议。它主要用于在流媒体系统中控制实时数据&#xff08;如音频、视频等&#xff09;的传输&#…

Python学习-----1.认识Python

目录 前言 1.关于Python博客前期的内容 2.计算机基础概念 2.1.什么是计算机? 2.2.什么是编程&#xff1f; 2.3.编程语言有哪些&#xff1f; 3.Python背景知识 3.1.Python是怎么来的&#xff1f; 3.2.Python都可以用来干什么&#xff1f; 3.3.Python的优缺点 3.4.Py…

MongoDB频繁掉线频繁断开服务的核心原因以及解决方案-卓伊凡|贝贝|莉莉|糖果

MongoDB频繁掉线频繁断开服务的核心原因以及解决方案-卓伊凡|贝贝|莉莉|糖果查看日志内容 &#xff1a;2025-07-22T17:05:20.2160800 I CONTROL [initandlisten] MongoDB starting : pid34231 port28018 dbpath/data/mongodb 64-bit hostVM-0-17-centos 2025-07-22T17:05:20.21…