本文将通过如下简单的代码来分析HashMap的内部数据结构的变化过程。

public static void main(String[] args) {Map<String, String> map = new HashMap<>();for (int i = 0; i < 50; i++) {map.put("key" + i, "value" + i);}
}
复制代码

1 数据结构说明

HashMap中本文需要用到的几个字段如下:

下面说明一下几个字段的含义

1)table

// HashMap内部使用这个数组存储所有键值对
transient Node<K,V>[] table;
复制代码

Node的结构如下:

可以发现,Node其实是一个链表,通过next指向下一个元素。

2)size

记录了HashMap中键值对的数量

3)modCount

记录了HashMap在结构上更改的次数,包括可以更改键值对数量的操作,例如put、remove,还有可以修改内部结构的操作,例如rehash。

4)threshold

记录一个临界值,当已存储键值对的个数大于这个临界值时,需要扩容。

5)loadFactor

负载因子,通常用于计算threshold,threshold=总容量*loadFactor。

2.new HashMap

new HashMap的源码如下:

/**
* The load factor used when none specified in constructor.
* 负载因子,当 已使用容量 > 总容量 * 负载因子 时,需要扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
复制代码

此时,HashMap只初始化了负载因子(使用默认值0.75),并没有初始化table数组。 其实HashMap使用的是延迟初始化策略,当第一次put的时候,才初始化table(此时table是null)。

3.table数组的初始化

当第一次put的时候,HashMap会判断当前table是否为空,如果是空,会调用resize方法进行初始化。 resize方法会初始化一个容量大小为16 的数组,并赋值给table。 并计算threshold=16*0.75=12。 此时table数组的状态如下:

4.put过程

map.put("key0", "value0");
复制代码

首先计算key的hash值,hash("key0") = 3288451 计算这次put要存入数组位置的索引值:index=(数组大小 - 1) & hash = 3 判断 if (table[index] == null) 就new一个Node放到这里,此时为null,所以直接new Node放到3上,此时table如下:

然后判断当前已使用容量大小(size)是否已经超过临界值threshold,此时size=1,小于12,不做任何操作,put方法结束(如果超过临界值,需要resize扩容)。

继续put。。。

map.put("key1", "value1");
复制代码

map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
map.put("key4", "value4");
map.put("key5", "value5");
map.put("key6", "value6");
map.put("key8", "value7");
map.put("key9", "value9");
map.put("key10", "value10");
map.put("key11", "value11");
复制代码

此时size=12,下一次put后size为13,大于当前threshold,将触发扩容(resize)

map.put("key12", "value12");
复制代码

计算Key的hash值,hash("key12")=101945043,计算要存入table位置的索引值 = (总大小 - 1) & hash = (16 - 1) & 101945043 = 3 从目前的table状态可知,table[3] != null,但此时要put的key与table[3].key不相等,我们必须要把他存进去,此时就产生了哈希冲突(哈希碰撞)。

这时链表就派上用场了,HashMap就是通过链表解决哈希冲突的。 HashMap会创建一个新的Node,并放到table[3]链表的最后面。 此时table状态如下:

5.resize扩容

此时table中一共有13个元素,已经超过了threshold(12),需要对table调用resize方法扩容。 HashMap会创建一个容量为之前两倍(16 2=32)的table,并将旧的Node复制到新的table中,新的临界值(threshold)为24(32 0.75)。

下面主要介绍一下复制的过程(并不是原封不动的复制,Node的位置可能发生变化)

先来看源码:

for (int j = 0; j < oldCap; ++j) { // oldCap:旧table的大小 =16Node<K,V> e;if ((e = oldTab[j]) != null) { // oldTab:旧table的备份oldTab[j] = null;// 如果数组中的元素没有后继节点,直接计算新的索引值,并将Node放到新数组中if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 忽略这个else if。其实,如果链表的长度超过8,HashMap会把这个链表变成一个树结构,树结构中的元素是TreeNodeelse if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 有后继节点的情况else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 【说明1】if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;//【说明2】newTab[j + oldCap] = hiHead;}}}
}
复制代码

【说明1】遍历链表,计算链表每一个节点在新table中的位置。

计算位置的方式如下:

1)如果节点的 (hash & oldCap) == 0,那么该节点还在原来的位置上,为什么呢?

因为oldCap=16,二进制的表现形式为0001 0000,任何数&16,如果等于0,那么这个数的第五个二进制位必然为0。

以当前状态来说,新的容量是32,那么table的最大index是31,31的二进制表现形式是00011111。 计算index的方式是 hash & (容量 - 1),也就是说,新index的计算方式为 hash & (32 - 1) 假设Node的hash = 01101011,那么

01101011
& 00011111
----------00001011 = 11
复制代码

2)下面再对比(hash & oldCap) != 0的情况

如果节点的(hash & oldCap) != 0,那么该节点的位置=旧index + 旧容量大小

假设Node的hash = 01111011,那么

01111011
& 00011111
----------00011011 = 27
复制代码

上一个例子的hash值01101011跟这个例子的hash值01111011只是在第5位二进制上不同,可以发现,这两个值在旧的table中,是在同一个index中的,如下:在此我向大家推荐一个架构学习交流裙。交流学习裙号:687810532,里面会分享一些资深架构师录制的视频录像

01101011
& 00001111
----------00001011 = 11
复制代码
01111011
& 00001111
----------00001011 = 11
复制代码

由于扩容总是以2倍的方式进行,也就是:旧容量 << 1,这也就解释了【说明2】,当(hash & oldCap) != 0时,这个Node的新index = 旧index + 旧容量大小。

扩容后,table状态如下所示:

最终,重新分配完所有的Node后,扩容结束。

结构体中初始化vector resize_Java-深入HashMap原理及内部存储结构相关推荐

  1. Java HashMap原理及内部存储结构

    本文将通过如下简单的代码来分析HashMap的内部数据结构的变化过程. public static void main(String[] args) {Map<String, String> ...

  2. c语言结构体中整形数组初始化,c – 将{0,0}在结构体中初始化数组?

    线 A C = {0,0}; 8.5.1聚合[dcl.init.aggr] / 12 Braces can be elided in an initializer-list as follows. I ...

  3. C# 8: 可变结构体中的只读实例成员

    在之前的文章中我们介绍了 C# 中的 只读结构体(readonly struct)[1] 和与其紧密相关的 in 参数[2]. 今天我们来讨论一下从 C# 8 开始引入的一个特性:可变结构体中的只读实 ...

  4. FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)

    ===================================================== FFmpeg的库函数源代码分析文章列表: [架构图] FFmpeg源代码结构图 - 解码 F ...

  5. 驱动下通过进程PID获得进程名 (动态获取ImageFileName在EPROCESS结构体中的相对偏移)...

    思路 进程EPROCESS结构体中含有进程名ImageFileName(需求处ImageFileName在EPROCESS结构体中的相对偏移)-->获得进程EPROCESS-->通过进程句 ...

  6. C++ 基础入门 之 结构体/结构体定义和使用/结构体数组/结构体指针/ 结构体嵌套结构体/结构体做函数参数/结构体中 const 使用场景/结构体案例

    C++ 基础入门 之 结构体/结构体定义和使用/结构体数组/结构体指针/ 结构体嵌套结构体/结构体做函数参数/结构体中 const 使用场景/结构体案例 目录 一.简单介绍 二.结构体定义和使用 三. ...

  7. C语言中的结构体,结构体中数组初始化与赋值

    最近写c语言中的结构体遇到了些问题,从网上找了些资料如下: 结构体是连续存储的,但由于结构体中成员类型各异,所以会存在内存对齐问题,也就是内存里面会有空档,具体的对齐方式这里 暂不讨论: 1.结构体的 ...

  8. c语言中 定义结构体变量初始化,c++中的结构体:声明 定义 初始化

    什么是结构体? 之前的学习中我们知道了数组是一个容器,而且是存放固定大小数据的容器,而且存放的元素的数据类型必须要一致. 比如数据库中有这样的一条记录学号 性别 年龄 成绩 地址应该怎样存放 结构体: ...

  9. 结构体中函数指针初始化

    /*** 为结构体中的指针数组赋值*/#include <stdio.h>typedef struct test {void (*p)(void);void (*q)(void);void ...

最新文章

  1. 《JavaScript应用程序设计》一一2.3 lambdas
  2. 【Cubase】Cubase 量化设置 ( 量化预置 | 长度量化 | 快捷键设置 | 量化开头 | 量化 MIDI 事件结尾 | 量化 MIDI 事件长度 )
  3. VS2010中整理代码快捷键
  4. python怎么字体加阴影_如何添加阴影到tkinter帧?
  5. c#操作数据库(二)dataAdapter篇
  6. 弹出键盘windowsoftinputmode属性设置值
  7. 数组:完成等差等比数列,及其他数列
  8. php order by where,无合适where条件过滤时尽量选择order by后的字段以驱动表进行查询...
  9. 如何让页面初始化的时候实现点击事件_辅助程序实现黑盒自动化测试的常见问题...
  10. 青铜到王者,看看你的MySQL数据库是什么段位,如何提升?
  11. Linux初级入门(第一次作业)
  12. 第九周LINUX 学习笔记
  13. Matplotlib笔记(1)——文字、绘图、背景(颜色对照表)
  14. 罗马数字转换python_Python实现将罗马数字转换成普通阿拉伯数字的方法
  15. 【多线程】多线程到底是个甚——多线程初阶(复习自用)
  16. Python爬虫入门教程 89-100 定个小目标,先用Python爬个一亿B站用户
  17. print中的逗号“,”打印出来相当于空格
  18. mac上的Finder快捷键汇总
  19. AutoML-第七章-AutoNet
  20. 中国网民对阿里巴巴的外资大股东感到愤懑

热门文章

  1. Spark.2.2源码阅读: SPARK SUBMIT任务提交
  2. 【MySQL】MySQL异常Lock wait timeout exceeded try restarting transaction
  3. 一次kafka的offset回退事件及相关知识点
  4. spark学习-JavaRDD注册成表然后用SparkSQL查询
  5. dell 如何给raid分区_用U盘启动盘给Dell服务器装系统找不到RAID阵列解决办法
  6. linux ls 目录结构,linux 系统目录结构 ls命令 文件类型 alias命令
  7. MySQLi学习笔记 :一 1. 数据库的基本概念 2. MySQL数据库软件 安装-- 卸载--. 配置 3. SQL
  8. 2054无法登陆mysql_张虹亮'blog » ubuntu20.04安装mysql8之后,php5程序和phpmyadmin出现#2054 无法登录MySQL服务器的解决方案...
  9. MySQL学习-子查询及limit分页
  10. python从入门到大神---4、python3文件操作最最最最简单实例