本文将通过如下简单的代码来分析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会创建一个容量为之前两倍(162=32)的table,并将旧的Node复制到新的table中,新的临界值(threshold)为24(320.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中的,如下:

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

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

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

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


欢迎关注我的微信公众号

转载于:https://juejin.im/post/5c41d0f8f265da615115033d

Java HashMap原理及内部存储结构相关推荐

  1. 结构体中初始化vector resize_Java-深入HashMap原理及内部存储结构

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

  2. 数据库内部存储结构探索

    在这篇文章中,我将会讲解一些数据库存储的内部机制,数据库是如何进行优化操作来提供惊人速度及其优势和缺点.  当我们谈起数据库内部存储结构时,人们都会想到B树或者B+树,但是我们在这里并不会谈论这些数据 ...

  3. PyTorch 笔记(11)— Tensor内部存储结构(头信息区 Tensor,存储区 Storage)

    1. Tensor 内部存储结构 tensor 数据结构如下图所示,tensor 分为头信息区(Tensor)和存储区 (Storage),信息区主要保存着 Tensor 的形状(size).步长(s ...

  4. Java HashMap 原理

    原文:https://tryenough.com/java-hashmap 本文涉及HashMap的: HashMap的简单使用 HashMap的存储结构原理 HashMap的扩容方法原理 HashM ...

  5. Java实现队列 链式存储结构

    2019独角兽企业重金招聘Python工程师标准>>> 队列的链式存储结构 示例代码如下, package hash;/*** Created with IntelliJ IDEA. ...

  6. java浮点数原理,浮点型数据存储原理

    进制基础 1.十进制:逢十进一 基数:10 权:    10^n 科学计数:527=5*10^2+2*10^1+7*10^0 2.二进制:逢二进一 基数:2 权:    2^n 转十进制:110(2) ...

  7. Zookeeper原理分析之存储结构ZkDatabase

    ZKDatabase在内存中维护了zookeeper的sessions, datatree和commit logs集合. 当zookeeper server启动的时候会将txnlogs和snapsho ...

  8. Java HashMap工作原理深入探讨

    大部分Java开发者都在使用Map,特别是HashMap.HashMap是一种简单但强大的方式去存储和获取数据.但有多少开发者知道 HashMap内部如何工作呢?几天前,我阅读了java.util.H ...

  9. java使用xml存储数据_聊一聊 Redis 数据内部存储使用到的数据结构

    Redis 数据库虽然一直都在使用,但是对其内部存储结构之类的,都没有研究过,哪怕是面试的时候都没有准备过这方面的东西.最近在看一门网课,里面有讲到过这一块的内容,结合了<Redis 设计与实现 ...

最新文章

  1. dom 元素拖拽实现
  2. struts2文件上传大小限制问题小结(引用)
  3. 页面某一个元素跟随输入框输入内容动态变化
  4. linux 文件打开数设置, too ma
  5. cocos2dx android运行Luac编译后的lua代码
  6. linux中的定时器检测按键,STM32单片机利用定时器实现按键采集
  7. Keras基础项目实践
  8. .net开源CMS系统使用教程之:如何用We7 CMS建设全新网站
  9. word流程图怎么使箭头对齐_word里流程图的直角箭头怎么画
  10. python 中in
  11. LollipopGo分布式架构--DB反向代理服务器设计
  12. 报错:Exception opening socket
  13. matlab模拟塞曼图谱,塞曼效应实验中法布里-珀罗标准具的Matlab模拟
  14. 【T3】win10系统成功注册加密锁后,登录软件依然提示“产品未找到合法的license授权”
  15. Spring Boot 学习[四] web项目实战训练(增删改查,分页,排序)
  16. Vue组件实现数字滚动抽奖效果
  17. 亚特兰提斯之人鱼宝宝
  18. app怎么调用mysql数据_教你如何拿别人APP中的数据
  19. STM32外部中断总结
  20. RK3399平台开发系列讲解(IIO子系统)4.43、IIO数据的获取方式介绍

热门文章

  1. 全球及中国汽车轮胎再制造市场销售产值与运营发展模式分析报告2022年
  2. 2022年全球及中国商业净水器行业十四五运营方向与盈利前景分析报告
  3. 全球及中国数字电视产业盈利模式及发展动态建议报告2021-2027年
  4. 中国钠离子电池行业竞争需求状况及投资盈利分析报告2021-2027年版
  5. 全球及中国软件外包行业“十四五”展望发展建议及创新布局战略报告2021-2027年
  6. 传统外贸不好做,为什么不来做跨境电商?
  7. 高级用户 java_java高级-基本
  8. java四种线程池类型以及可选择的阻塞队列
  9. 改变进程的优先级,nice,getpriority,setpriority
  10. memmove函数使用时注意的问题