[Java] HashMap是如何实现的?扩容机制是什么?树化机制知道吗?结合源码带你理解HashMap的原理。

news2024/10/6 14:36:05

文章目录

  • 前言
  • HashMap是什么?
    • Map接口
    • 散列表(HashTable)
  • HashMap的扩容机制
    • 扩容机制?扩谁的容?
    • HashMap的容量(Capacity)属性
    • HashMap的扩容因子(load factor)属性
  • HashMap的树化机制
    • 触发树化机制的条件
    • 反树化机制
  • HashMap是如何实现扩容的?
    • 为什么HashMap的容量一定是2的N次方?
  • 结语

前言


HashMap可以说是一个经常被谈论到的集合类了,笔者也一度考虑是否要写这篇文章。毕竟做源码分析、谈论过HashMap实现的人也很多了。不过为了在未来的文章中更好的讲解ConcurrentHashMap,笔者还是决定为其写一篇基础性的前置知识文章。

HashMap是什么?


万变不离其宗,要理解HashMap的原理和实现,我们必须要知道HashMap是个什么东西

HashMap是jdk java.util包下的一个集合类。主要是利用散列表(HashTable) 这个 数据结构 来实现 Map接口
和ArrayList与LinkedList一样,都是用某种特定的技术去实现某个接口,比如这里的List接口前者用数组实现,后者用链表实现。同样实现了Map接口的兄弟类也是存在的,那就是利用 平衡二叉搜索树(BBST) 来实现的 Map接口的 java.util.TreeMap。

所以不难看出,HashMap有两个关键词:散列表Map接,那么我们分别来讲解一下这两个概念。

Map接口

Map接口大家应该都不陌生,大部分编程语言里都提供Map或Dictionary功能,其核心功能是提供键值对(Key-Value Pair)的增删改查。下面截图截取了部分Map接口的方法,其中核心方法有:

  • get(Object) : 数据查询
  • put(K, V):数据增改
  • remove(Object):数据删除

Map接口的截图

散列表(HashTable)

HashMap的Hash和ArrayList的Array一样,是取自其依赖的底层技术:散列表(HashTable)。散列表的基本思想是把不同Key的哈希值通过取模给放到不同的哈希槽(Hash Slot)里存放。这样下次查询的时候可以再通过哈希值取模快速找到存放数据的哈希槽。这样能大大加快查找和插入数据的效率。

试想一下,假设你有100万条数据,1万个哈希槽,运气很好每个哈希槽平均分到100个数据。这样如果你通过哈希值取模只需要O(1)的时间复杂度就能找到对应的哈希槽,而最坏的遍历整个哈希槽也不过花费O(n)的时间,这里n是100。当然JDK对哈希槽的存储查询还做了优化,肯定不是O(n)的复杂度,提一个关键词:树化,这个我们后面的章节再讨论。

这里笔者也只是简单的介绍了以下散列表是什么。如果想详细了解散料表的,可以看笔者之前写的这篇文章:《数据结构之 - 散列表(Hash table)》

散列表截图

HashMap的扩容机制

HashMap作为一个容器类,绕不开的就是如何进行扩容。那么如何进行扩容这个事儿呢,也就是我们一直提到的扩容机制,亦或是可以理解为扩容策略

扩容机制?扩谁的容?

对于HashMap这个容器类来说,我们前面也提到了,其内部依赖的数据结构是散列表(HashTable)。散列表本本质是一个数组(一堆哈希槽)。所以其实扩容的中心思想也很简单,对于HashMap来说,扩容的本质就是增加散列表的大小(增加哈希槽数量)

HashMap的容量(Capacity)属性

既然涉及到扩容(扩展容量),就不得不提HashMap的容量(Capacity)这个属性了。

HashMap的table属性

在HashMap中,容量这个属性是由table这个Node数组来保持的。如下图所示,如果想获取容量信息那么直接通过table.length 获取即可,但也因为table这个数组是懒汉初始化的,有可能为null,需要做null判定。

HashMap的resize方法摘要

前面我们提到table是懒汉初始化的,意味着在HashMap构造方法里是不会直接去申请table所需内存的。
那么如果我们通过HashMap的构造方法指定了 初始容量(initialCapacity) 会发生什么呢

HashMap构造方法

可以看到我们的构造方法利用 tableSizeFor(int) 这个方法去计算了一个容量值存放到了threshold属性里。
而这个属性呢,一般是代表触发下一次扩容的阈值(the next size value at which to resize),这个值一般是容器 x 扩容因子(capacity * load factor)但因为懒汉初始化,在第一次申请table数组内存之前,被临时征用于存储初始化容量信息,也就是第一次申请table数组的大小。

HashMap的threshold属性

而tableSizeFor(int)方法,作为计算容量的方法,一定会返回一个2的N次方的容量值(Returns a power of two size for the given target capacity)

在这里插入图片描述
对位运算不熟悉的朋友们呢,可以这么理解 -1 的二进制是 0b11111111_11111111_11111111_11111111。上面的运算是右移(容器减一)的先导零(Leading zero)次,笔者做了个表来方便大家理解。

capcap - 1先导零n返回值
16 (0b00000000_00000000_00000000_00010000)15(0b00000000_00000000_00000000_00001111)28个1516
20 (0b00000000_00000000_00000000_00010100)19(0b00000000_00000000_00000000_00010011)27个3132
31 (0b00000000_00000000_00000000_00011111)30(0b00000000_00000000_00000000_00011110)27个3132

可以看出,我们在构造器中设置的initialCapacity值最终都会变成其最匹配的2的N次方的值。当然如果值大于最大容量(MAXIMUM_CAPACITY)限制则会返回MAXIMUM_CAPACITY,这个值是2的30次方,大约是10亿多,也就是实际上我们HashMap的容量一定是2的N次方

HashMap的MAXIMUM_CAPACITY

HashMap的扩容因子(load factor)属性

HashMap只有在达到某种条件下,才会触发扩容处理,而这个条件就和扩容因子有关

HashMap的扩容因子属性 loadFactor
前面的章节我们提到了threshold(扩容阈值)属性,这个属性的值则是通过 capacity * loadFactor 这个公式来计算出来的,这个在我们的resize()方法中能够看到,笔者截取其中一段:

threshold计算逻辑

可以看到这段代码:threshold = newThr = ft = newCap * loadFactor;
在具体扩容的判定时,当 size 大于扩容阈值就会触发扩容。size呢则是元素(键值对)的个数了。也就是说假设你有64个哈希槽,你只用了1个哈希槽,里面有超过48个元素(默认扩容因子计算得出阈值是48)时,也会触发扩容,而不是说你用了48个哈希槽才会触发扩容。

扩容触发逻辑

HashMap为我们提供了默认的扩容因子,其值为 0.75f 。也就是说如果你不设置扩容因子使用默认扩容因子时,当你HashMap内键值对数量超过容量的 75% 时就会触发扩容。

HashMap默认的扩容因子

HashMap的树化机制


在了解HashMap是如何实现扩容之前我们还需要了解,HashMap的树化机制。前面我们简要提到了哈希槽(Hash Slot)的概念,对于Key的哈希值取模后,会把数据放到对应的哈希槽里。而这个哈希槽,也需要用某种数据结构来存储键值对数据。

哈希槽内数据量比较小的时候,HashMap会用链表来存储同一个哈希槽内的数据。链表的Node类定义如下,不难看出其是一个单向链表(仅有next指针):

HashMap的Node类

哈希槽内数据比较大的时候,HashMap会用红黑树(一种平衡二叉搜索树) 来存储哈希槽内的数据,以便提升数据查询和插入等操作的性能(从O(n)优化到O(log n))。树化后的TreeNode类定义如下:

HashMap的TreeNode类

触发树化机制的条件

和扩容机制一样,触发树化机制也需要有一定的条件。主要是两点,

  1. 一是哈希槽内数量量需要达到一定规模(槽内数据量(bin count) >= TREEIFY_THRESHOLD)。
  2. 二是整个HashMap的容量需要达到最小树化阈值(Capacity >= MIN_TREEIFY_CAPACITY)

哈希槽内触发树化机制的阈值 TREEIFY_THRESHOLD 的值是 8。也就是当哈希槽内数据量大于等于8时就会触发树化判定机制

HashMap的TREEIFY_THRESHOLD属性

当触发了树化判定机制后,HashMap会检查HashMap容量是否达到最小树化阈值 (MIN_TREEIFY_CAPACITY,值为64),如果达到了这个条件就把当前哈希槽的数据结构从链表树化为红黑树,如果达不到这个条件就会触发扩容。

HashMap的MIN_TREEIFY_CAPACITY属性

相关源码如下,如果binCount > 8 (binCount是从0开始计数,实际树化条件是 >= 8)。

触发树化判定机制

在treeifyBin方法(树化判定方法),最初还会进行一次容量条件的检查,容量不达标就会进行扩容(resize),容量达标则进行树化处理。

treeifyBin方法

反树化机制

其实除了数据量变大后会有树化机制之外,我们的HashMap里的特定哈希槽里的数据是可以增加也可以减少的(删减数据 或 扩容)。那么当红黑树的数据量减少到一定程度时,也会触发反树化机制。与树化机制类似,也用常量定义了反树化阈值(UNTREEIFY_THRESHOLD),其值为6。

HashMap的UNTREEIFY_THRESHOLD属性

HashMap是如何实现扩容的?


在前面章节我们了解了HashMap扩容相关的关键属性(扩容阈值threshold、容量capacity与扩容因子load factor)、触发扩容的条件(size > threshold)以及树化/反树化机制。现在我们来看一看到底HashMap是如何实现扩容的。

那么其实HashMap扩容的目标其实很简单,就是把内部散列表的大小倍增。倍增了之后哈希槽的数量随之倍增,原本分布在这些老哈希槽内的数据也需要被重新调整至新的哈希槽内。因为哈希槽的数据结构有链表和红黑树两种,倍增后红黑树的大小也大概率会减小,因此伴随着还有反树化处理。

所以实际上扩容就是以下几个处理:

  1. 新申请一个两倍大数组(下图newTab)用于替换原数组(下图oldTab)。
  2. 把原数组的链表 或 红黑树数据放到新数组的链表 或 红黑树里。
  3. 新红黑树数据结构如果数据量太小,则对其做反树化。

resize方法摘要

为什么HashMap的容量一定是2的N次方?

这个和HashMap的扩容机制实现有关了,HashMap的实现是倍增。意味着原本容量n的散料表的第i个哈希槽内的数据,倍增扩容为2n大小后,原本第i个哈希槽内的数据一定会落在第i个哈希槽或第i + n个哈希槽内。2的N次方有助于HashMap扩容的简单实现而不用考虑扩容后新哈希槽内数据分配不均,也能避免额外的树化处理开销等,同时链表对于的新哈希槽数据结构一定是链表,红黑树对应的新哈希槽数据结构也一定是红黑树,简化了HashMap的实现代码。

具体可以看下图的newTab[j] 和 newTab[j + oldCap]部分:

链表扩容
这是链表部分的处理,红黑树部分的处理(tab[index] 和 tab[index + bit])也是一样:

红黑树扩容

结语


总的来说,HashMap的实现逻辑还是比较简单的,内部是散列表,散列表本质则是数组,散列表数组内的每一个元素都是一个哈希槽,多个哈希值取模后相同的数据会被存储在相同的哈希槽内,为了存储这些数据HashMap使用了链表和红黑树这两种数据结构,它们在数据量变化时通过树化反树化机制来互相转换。而扩容机制则是把HashMap内部的散列表进行倍增的一种处理。

我是虎猫,希望本文能对你有所帮助。(=・ω・=)

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

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

相关文章

【js逆向基础】crypto 之 hash和hmac

▒ 目录 ▒🛫 导读需求开发环境1️⃣ hash对象创建Hash常见方法例封装成 stream 实例封装成管道流(piped stream)2️⃣ hmac对象创建Hmac常见方法例封装成 stream 实例封装成管道流(piped stream)🛬 文章小结📖 参考资…

mysql学习---流程控制函数(条件判断)case when ,if使用

文章目录流程控制函数流程控制函数的介绍流程函数的使用流程控制函数 流程控制函数的介绍 if(value,value1,value2) 解释:如果value的结果为true ,返回value1,否则返回value2 例如: select if(1>0,正确,错误) ->正确ifnull(value1,val…

Babel快速入门

一、简介 Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行执行。 这意味着,你可以现在就用 ES6 编写程序,而不用担心现有环境是否支持。 二、安装 安装命令行转码工具 Babel提供babel-cli工具&#xff…

Mysql调优(三)——通过索引进行优化、 查询优化

接上:Mysql调优(二)——schema与数据类型优化、执行计划 本篇文章在一定程度上与下面两篇文章重合,笔者建议优先读下面两篇文章,本文偏向概念,而它们偏向原理: Mysql索引的本质深入浅出&#x…

全栈Jmeter接口测试(五):json提取器元件及jsonpath介绍,响应断言元件

Jmeter(10):json提取器元件及jsonpath介绍 json提取器元件介绍: json提取器与正则表达式提取器功能类似,也是用来截取响应信息的部分保 存到指定的变量中去,不同的是,它只能用来处理响应正文,并且响应正文…

ITE Super IO 学习 – 串口

串口都算是一个比较古老的功能了,也就是比较成熟了。Super IO中的大部分是硬件已经做好的,BIOS这边只需做小小的改动甚至默认便可以工作了。 串口寄存器 串口有关的寄存器总的来说有4个,两个用来设置IO资源的地址,一个是中断号选择,还有一个特殊配置寄存器。 Seri…

高级路由期末命令配置

文章目录前提要求(1)拓扑图搭建及IP地址规划拓扑图配置:IP地址表规划表1网络连接规划表表2 网络设备明细表表3 IP规划表(2)OSPF(3)BGP(4)路由引入(5)路由选择&#xff08…

PicoDet代码学习记录

推理步骤 [picodet_s_320_coco.yml] Infer.py main()->run()Trainer.py __init__() self.model create(cfg.architecture) Picodet.py from_config()->__init__() head create(cfg[head], **kwargs)时候调用: Layers.py MultiClassNMS __…

Django(一)

一、Django介绍 1、python语言编写的开源web框架 2、重量级python web框架,配备了常用的大部分组件 3、组件: ※基本配置文件/路由系统 ※模型层(M)/模板层(T)/视图层(V) ※co…

Cadence创建插件焊盘(超详细)

背景:Cadence画封装,在我看来,操作是比较不方便的事儿,所以在此在记录一遍。要画一个插件封装,需要调用画好的焊盘(AD里直接设置即可),另外放置好pin的位置和绘制好边框。包括遇到的…

SpringBoot集成阿里EasyExcel导出excel高级实战

目录参考一、引入包二、导出到文件并输出到后台三、过滤字段方式1:类上加注解 ExcelIgnoreUnannotated,过滤属性没有ExcelProperty注解的字段方式2:指定字段加注解方式3:代码指定过滤字段, 同一个excel生成两个sheet分别过滤不同字…

clickhouse集群搭建

一、clickhouse单机部署 clickhouse镜像下载地址 https://mirrors.tuna.tsinghua.edu.cn/clickhouse/ 1、关闭防火墙 #查看防火墙状态systemctl status firewalld#关闭防火墙systemctl stop firewalld#设置防火墙服务开机不自启systemctl disable firewalld#再次查看防火墙检…

开源软件如何使企业和业务受益

在当今技术驱动、快速发展的商业环境中,越来越多的公司选择投资开源软件。开源为企业提供了许多优势:更快的创新步伐、来自庞大而热情的社区的强大支持以及不受供应商锁定的影响。 对于许多人来说,开源不仅仅是编码。它代表了一种生活方式。…

使用IDEA工具,通过Java API 操作 HDFS (文件/目录的操作,含源码,详细操作步骤)

文章目录一,了解 HDFS Java API(一)HDFS常见类与接口(二)FileSystem的常用方法二,编写Java程序访问HDFS01 创建Maven项目02 添加相关依赖03 创建日志属性文件(1)在resources目录里创…

2022年还剩半个月,我从外包公司离职了...

今天是12.12,距离元旦也仅剩半个月,大家都在考论假期怎么过,没错,我离职了... 19年大专毕业通过校招进入了一家外包公司,干了接近3年的功能测试,今年年初,感觉自己不能够在这样下去了&#xff…

MyBatis 注解开发

文章目录一、单表查询1. Select 注解2. Insert 注解3. Update 注解4. Delete 注解5. Param 注解二、多表关联查询1. 一对一查询2. 一对多查询3. 多对多查询一、单表查询 1. Select 注解 前面的章节介绍了 MyBatis 的基本用法、关联映射、动态 SQL 和缓存机制等知识&#xff0…

[附源码]计算机毕业设计房屋租赁系统Springboot程序

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: Springboot mybatis MavenVue等等组成,B/S模式…

算法竞赛入门【码蹄集进阶塔335题】(MT2201-2225)

算法竞赛入门【码蹄集进阶塔335题】(MT2201-2225) 文章目录算法竞赛入门【码蹄集进阶塔335题】(MT2201-2225)前言为什么突然想学算法了?为什么选择码蹄集作为刷题软件?目录1. MT2201 字符串转换2. MT2202 Summer Pockets3. MT2203…

alsa框架与音频芯片移植基础

ALSA音频框架 Alsa是Advanced Linux Sound Architecture的缩写,即高级Linux声音架构,在Linux操作系统上提供了对音频和MIDI的支持。在Linux 2.6的内核版本后,Alsa目前已经成为了linux的主流音频体系结构。 除了 alsa-driver,ALS…

CSS -- 使用纯CSS绘制三角形及常见案例汇总

文章目录1. 绘制原理2. 绘制最简单的三角形3. 绘制对话框4. 绘制两条直角边不等长的三角形1. 绘制原理 网页中常见一些三角形,使用 CSS 直接画出来就可以,不必做成图片或者字体图标 给每个边框不同的颜色可以看到,每个边框其实都是一个小三…