hashmap底层原理解析

news/2024/4/30 14:21:24/文章来源:https://blog.csdn.net/m0_47498874/article/details/126639280

底层数据结构,1.7与1.8有何不同?

为何要用红黑树,为何一上来不树化?树化阈值为何是8?何时会树化?何时会退化为链表?

链表比较短的时候,查询性能并没有那么低,不用费劲把它转成红黑树,还浪费内存

HashMap

要求

  • 掌握 HashMap 的基本数据结构
  • 掌握树化
  • 理解索引计算方法、二次 hash 的意义、容量对索引计算的影响
  • 掌握 put 流程、扩容、扩容因子
  • 理解并发使用 HashMap 可能导致的问题
  • 理解 key 的设计

1、基本数据结构

  • 1.7 数组 + 链表
  • 1.8 数组 + (链表 | 红黑树)

因为红黑树是自平衡二叉树。查询效率比较稳定;所以不用AVL平衡树

2、树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击(制造大批hash值相同的数据),防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O(1)O(1)O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n)O(log_2⁡n )O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006树化阈值选择 8 就是为了让树化几率足够小
    举例
    在一个存有二十多万个单词的文件,把他们读到hashmap集合中,没有出现树化情况,可见hashmap底层解决冲突很有一手,出现树化是不正常的情况。
    在这里插入图片描述

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

因为扩容后,计算下标是用hash值模数组长度,所以链表会缩短;但如果hash值原先都一样,再怎么扩容,长度也不会缩短。

在这里插入图片描述
按照字符串排序的(首字母),所以’10’<‘2’

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,(移除之前)若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表

3、索引计算

索引计算方法

  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希
    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

4、put 与扩容

put 流程

  1. HashMap 是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
  3. 如果桶下标还没人占用,创建 Node 占位返回
  4. 如果桶下标已经有人占用
    1. 已经是 TreeNode 走红黑树的添加或更新逻辑
    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
  5. 返回前检查容量是否超过阈值,一旦超过进行扩容

1.7 与 1.8 的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

5、并发问题

扩容死链(1.7 会存在)

1.7 源码如下:

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;}}
}
  • e 和 next 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

在这里插入图片描述

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

在这里插入图片描述

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

在这里插入图片描述

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

在这里插入图片描述

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

在这里插入图片描述

数据错乱(1.7,1.8 都会存在)

  • 代码参考 day01.map.HashMapMissData,具体调试步骤参考视频

补充代码说明

  • day01.map.HashMapDistribution 演示 map 中链表长度符合泊松分布
  • day01.map.DistributionAffectedByCapacity 演示容量及 hashCode 取值对分布的影响
    • day01.map.DistributionAffectedByCapacity#hashtableGrowRule 演示了 Hashtable 的扩容规律
    • day01.sort.Utils#randomArray 如果 hashCode 足够随机,容量是否是 2 的 n 次幂影响不大
    • day01.sort.Utils#lowSameArray 如果 hashCode 低位一样的多,容量是 2 的 n 次幂会导致分布不均匀
    • day01.sort.Utils#evenArray 如果 hashCode 偶数的多,容量是 2 的 n 次幂会导致分布不均匀
    • 由此得出对于容量是 2 的 n 次幂的设计来讲,二次 hash 非常重要
  • day01.map.HashMapVsHashtable 演示了对于同样数量的单词字符串放入 HashMap 和 Hashtable 分布上的区别

6 key 的设计

key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然
  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
  3. key 的 hashCode 应该有良好的散列性

如果 key 可变,例如修改了 age 会导致再次查询时查询不到

public class HashMapMutableKey {public static void main(String[] args) {HashMap<Student, Object> map = new HashMap<>();Student stu = new Student("张三", 18);map.put(stu, new Object());System.out.println(map.get(stu));stu.age = 19;System.out.println(map.get(stu));}static class Student {String name;int age;public Student(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Student student = (Student) o;return age == student.age && Objects.equals(name, student.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);}}
}

String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 SiS_iSi,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S0∗31(n−1)+S1∗31(n−2)+…Si∗31(n−1−i)+…S(n−1)∗310S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 $32 ∗h -h $
    • 25∗h−h2^5 ∗h -h25hh
    • h≪5−hh≪5 -hh5h

ArrayList

要求

  • 掌握 ArrayList 扩容规则

扩容规则

  1. ArrayList() 会使用长度为零的数组

  2. ArrayList(int initialCapacity) 会使用指定容量的数组

  3. public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量

  4. add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍

  5. addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)

其中第 4 点必须知道,其它几点视个人情况而定

提示

  • 测试代码见 day01.list.TestArrayList ,这里不再列出
  • 注意的是,示例中用反射方式来更直观地反映 ArrayList 的扩容特征,但从 JDK 9 由于模块化的影响,对反射做了较多限制,需要在运行测试代码时添加 VM 参数 --add-opens java.base/java.util=ALL-UNNAMED 方能运行通过,后面的例子都有相同问题

代码说明

  • day01.list.TestArrayList#arrayListGrowRule 演示了 add(Object) 方法的扩容规则,输入参数 n 代表打印多少次扩容后的数组长度

Iterator

要求

  • 掌握什么是 Fail-Fast、什么是 Fail-Safe

Fail-Fast 与 Fail-Safe

  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败

  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

提示

  • 测试代码见 day01.list.FailFastVsFailSafe,这里不再列出

LinkedList

要求

  • 能够说清楚 LinkedList 对比 ArrayList 的区别,并重视纠正部分错误的认知

LinkedList

  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入删除性能高
  4. 占用内存多

ArrayList

  1. 基于数组,需要连续内存
  2. 随机访问快(指根据下标访问)
  3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
  4. 可以利用 cpu 缓存,局部性原理

代码说明

  • day01.list.ArrayListVsLinkedList#randomAccess 对比随机访问性能
  • day01.list.ArrayListVsLinkedList#addMiddle 对比向中间插入性能
  • day01.list.ArrayListVsLinkedList#addFirst 对比头部插入性能
  • day01.list.ArrayListVsLinkedList#addLast 对比尾部插入性能
  • day01.list.ArrayListVsLinkedList#linkedListSize 打印一个 LinkedList 占用内存
  • day01.list.ArrayListVsLinkedList#arrayListSize 打印一个 ArrayList 占用内存

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

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

相关文章

项目经理如何做好项目管理中的风险管理

项目中始终有些看不见的风险&#xff0c;这些风险可以成为威胁&#xff0c;也有可能会影响到项目的计划发生重大的变化。 一、项目目标不明确 确定项目目标是项目启动阶段重要的工作之一&#xff0c;要想项目在实施阶段少走弯路&#xff0c;在项目开工前&#xff0c;必须清晰…

信息系统项目管理师Part16-物联网

物联网 1.物联网的两项关键技术 传感器技术、嵌入式技术 2.传感器技术和嵌入式技术 RFID射频识别&#xff1a;可通过无线电信号识别特定目标并读写相关数据&#xff0c;而无需识别系统与特定目标之间建立机械或光学接触。 嵌入式技术&#xff1a;是综合了计算机硬件、传感器技…

猿创征文|OLAP之apache pinot初体验

目录 一、背景 二、介绍 三、特征 四、价值 五、参考组件 组件清单介绍&#xff1a; 1.Controller 2.Server 3.Broker 4.Minion (optional) 六、数据采集 批量数据流程 实时数据流程 查询处理流程 一、背景 最近在熟悉公司内部的埋点采集&#xff0c;发现数据架构…

NK-RTU980 CAP

BSP包中有两个CAP相关的例程&#xff0c;两个例程的区别为获取的图像数据的存储格式不同&#xff0c;planar例程是先存储所有像素点的Y&#xff0c;再存U&#xff0c;再存V。packed例程是每个像素的YVU连续存储。 一、硬件电路 处理器为NUC980DR61Y&#xff0c;封装为64pin&a…

python--转换wrf输出的风场数据为网页可视化的json格式

前言&#xff1a; 一般网页可视化风场中的数据都是json格式&#xff0c;而如果我们希望将wrf模式模拟输出的风场数据在网页中进行展示&#xff0c;这就需要先将wrfoutput数据转换为网页可以识别的json格式。 这里主要需要用到json库&#xff0c;主要的实现方式就是将读取的风场…

微信网课答案公众号题库接口使用

微信网课答案公众号题库接口使用 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查题校园题库&#xff1a;查题校园题库后台&…

亚马逊审核 美国站安全带ASTMF1772安全绳攀岩绳EN892认证流程

1 登山锁扣定义 登山用锁扣是一种带弹簧门的金属环状物&#xff0c;用于在攀岩和登山时快速可逆地连接各部件&#xff0c;是安全系统关键的一部分。 登山用锁扣可用于将绳索固定到设备上&#xff0c;或者将两件或多件设备连接在一起。它们通常由铝或钢制成。这种锁扣具有不同…

ps2021神经ai滤镜无法使用,ps2021没法用神经元滤镜

如何解决ps2021 新版 AI神经滤镜不能用? 网上买正版&#xff0c;更新下就好了&#xff0c;盗版的都会有各种这样的问题。ps2021神经AI滤镜是需简要上传云端&#xff0c;由Adobe官方服务器人工智能运算的。 Ps2021版本新增了Ai神经元滤镜&#xff0c;它不是与软件一起安装的&…

谣言粉碎机?Python验证股市操盘口诀

更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流。 经常炒股的朋友,应该都听说过这段操盘口诀: 早上大跌要买,早上大涨要卖 下午大涨不追,下午大跌次日买 早上大跌不割,不涨不跌睡觉 我们随手百度,也能发现各大主流论坛,充斥着该口…

Spring入门——Eclipse实现HelloWorld程序

前言 疫情影响又延期开学&#xff0c;只能在家上上网课划划水&#xff0c;刚做完spring入门的一个小作业&#xff0c;来做个总结分享&#xff0c;我也是个刚入门的小白&#xff0c;还望大佬们指点。 步入主题 环境 eclipse/spring-tool-suite-3 jdk1.8.0_221 另外&#xff0…

Linux :mysql数据库自动备份

Linux &#xff1a;mysql数据库自动备份前言使用shell脚本进行数据库的定时备份确定备份数据库备份shell脚本定时shell脚本前言 当项目发布到服务器上后&#xff0c;接下来考虑到就是如何做好数据库的数据备份。为的就是防止服务器突然异常崩溃&#xff0c;而导致的数据丢失问…

使用上下游思维实现系统解耦

在软件开发领域&#xff0c;解耦这个词相信大家都不陌生。在面向对象的语境下&#xff0c;我们会应用SOLID原则来构建高内聚低耦合的应用&#xff0c;实现模块间的解耦&#xff1b;在复杂业务系统分析和建模时&#xff0c;会通过DDD的战略和战术设计帮助划分领域并实现分布式系…

Java毕业设计-校园活动赞助与宣传管理系统

&#x1f525;作者主页&#xff1a;疯狂行者&#x1f525; &#x1f496;✌java领域优质创作者,专注于Java技术领域技术交流✌&#x1f496; &#x1f496;文末获取源码&#x1f496; 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1…

(分布式缓存)Redis持久化

一、RDB持久化 首先需要在Linux系统中安装一个Redis&#xff0c;如果尚未安装的同学&#xff0c;可以参考下面链接教程安装先&#xff1a; (73条消息) 单机安装Redis_其然乐衣的博客-CSDN博客 修改配置文件 创建一个数据 因为设置了只要5秒内有一次修改就会触发一次备份数据&am…

最全 Burp Suite 最新付费稳定版安装教程

介绍 Burp Suite是web应用程序渗透测试集成平台。从应用程序攻击表面的最初映射和分析,到寻找和利用安全漏洞等过程,所有工具为支持整体测试程序而无缝地在一起工作。 平台中所有工具共享同一robust框架,以便统一处理HTTP请求、持久性、认证、上游代理、日志记录、报警和可扩…

《QDebug 2022年8月》

一、Qt Widgets 问题交流 1.QWidget鼠标事件穿透 对于一些透明或者半透明的QWidget&#xff0c;可能需要点击其下方的按钮或其他组件&#xff0c;但是QWidget本身是会接收这些鼠标事件的&#xff0c;需要一些额外的处理。下面是百度到的一些方法&#xff1a; 方式A.设置setA…

Nacos下载和安装-windows

Nacos官网&#xff1a;https://nacos.io/zh-cn/ Nacos官方文档&#xff1a;https://nacos.io/zh-cn/docs/quick-start.html 一、下载 进入nacos官网&#xff0c;选择相应版本下载 github上nacos的zip资源&#xff0c;下载速度奇慢问题。 百度网盘&#xff1a;https://pan.b…

云原生游戏第 2 讲:OpenKruiseGame 设计理念详解

后疫情时代&#xff0c;游戏行业步入高质量发展期&#xff0c;游戏云原生化势在必行。不久前&#xff0c;针对游戏行业云原生落地的难点、游戏玩家服容器化的困境等问题&#xff0c;阿里云容器服务团队通过直播课程《云原生游戏第1讲&#xff1a;游戏玩家服容器化的困境与解法》…

PMP每日一练 | 考试不迷路-9.1(包含敏捷+多选)

&#xff01;PMP最新考试通知 &#xff01; ​2022年6-8月落考考生可免费重考一次&#xff01; 11月考试可以报名 ​&#xff08;9月考试改到11月) 每日5道PMP习题助大家上岸PMP&#xff01;&#xff01;&#xff01; ​1.项目经理接到一个开发新产品的项目&#xff0c;这…

一体式城市内涝监测站

一体式城市内涝监测站 计讯物联一体式城市内涝监测站&#xff0c;智能监测城市重点区域视频监控、水位、雨量、水量、流速等&#xff0c;目标数据实时上报云端&#xff0c;相关部门远程云平台同步监控(视频图像、水雨情、积水、排水工况)&#xff0c;智能化管理系统实现城市防…