0
点赞
收藏
分享

微信扫一扫

深入源码分析HashMap到底是怎样将元素put进去的

说明

此次深入源码解剖是为了搞明白几个问题
1、HashMap是如何初始化的
2、HashMap的扩容机制是怎样的
3、元素是如何​​put​​进HashMap的,具体位置在哪(重难点)
4、扩容后,元素是如何重新分布的(重难点)

注:为了方便读者复盘,我截取源码时会将源码行号也带上。

jdk版本:1.8

在深入源码之前,应该先有个大致的了解,在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构

HashMap结构示意图

深入源码分析HashMap到底是怎样将元素put进去的_hashmap
数组就是源码中的​​​table​

深入源码分析HashMap到底是怎样将元素put进去的_数组_02

链表就是内部类​​Node​​​深入源码分析HashMap到底是怎样将元素put进去的_后端_03

红黑树就是内部类​​TreeNode​​​深入源码分析HashMap到底是怎样将元素put进去的_java_04

解剖思路

先创建一个无参构造函数,往其内添加一个元素,看看HashMap是如何初始化的,然后再创建一个有参构造函数,并往其中添加若干元素,直至触发扩容机制

无参构造函数

深入源码分析HashMap到底是怎样将元素put进去的_java_05

在无参构造函数之前,打个断点,以​​debug​​​模式启动,关于调试键的使用请参照:IDEA调试键的说明,在此不再赘诉。

点击​​Step Over​​​到​​HashMap map = new HashMap();​​所在的这行

点击​​Force Step Into​​​,会发现先调用的是类加载器深入源码分析HashMap到底是怎样将元素put进去的_数组_06
这不是今天的重点,我们直接​​​Step Out​​​,然后再次点击​​Force Step Into​​​,进入源码
深入源码分析HashMap到底是怎样将元素put进去的_后端_07

初始化负载因子

这行代码的意思是​​loadFactor​​​设置为默认的负载因子 0.75,上面的因为注释说同时初始容量为16,这里暂时没看到在哪了赋值了
反倒是你在​​​475​​​行点击​​Force Step Into​​​会发现它调用的父类的构造函数
深入源码分析HashMap到底是怎样将元素put进去的_构造函数_08
这也补充一个知识点,有父类构造函数的先执行父类的构造函数,再执行自己的构造函数
执行完父类的构造函数,将负载因子设置为默认,初始化就此完成了
深入源码分析HashMap到底是怎样将元素put进去的_java_09

继续点击​​Force Step Into​​​,来到​​map.put("debug","HashMap");​​​深入源码分析HashMap到底是怎样将元素put进去的_hashmap_10

继续点击​​Force Step Into​​​,进入源码​​611​​​行
深入源码分析HashMap到底是怎样将元素put进去的_hashmap_11

计算key的hash值

继续点击​​Force Step Into​​​来到计算key的hash值
深入源码分析HashMap到底是怎样将元素put进去的_hashmap_12
继续点击​​​Force Step Into​​​会发现因为key的类型是String,最终调用的是String的hashCode,经过计算后key的hash值为​​95458899​​​深入源码分析HashMap到底是怎样将元素put进去的_后端_13
继续点击​​Force Step Into​​,算完key的hash值,然后调用putVul
深入源码分析HashMap到底是怎样将元素put进去的_构造函数_14

初始扩容

因为是刚刚构造的所以​​tab​​​为​​null​​​,所以要走一波​​resize()​​​深入源码分析HashMap到底是怎样将元素put进去的_java_15
点击​​Force Step Into​​进去看看,用红线框出执行路径
深入源码分析HashMap到底是怎样将元素put进去的_java_16
因为​​oldTab​​是为​​null​​的所以直接返回​​newTab​深入源码分析HashMap到底是怎样将元素put进去的_构造函数_17
回顾一下这段源码,虽然很长很复杂,但对于此次调试,核心的就是以下三行
深入源码分析HashMap到底是怎样将元素put进去的_数组_18
将容量设置为默认容量​​DEFAULT_INITIAL_CAPACITY​​为16,将扩容阈值设置为​​(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)​​是 16*0.75 = 12,然后以该容量(16)为长度创建一个链表数组​​Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]​​,并返回该数组

所以所谓的默认容量和默认扩容阈值是在插入第一个元素时,调用resize()方法赋值的,并不是用构造函数初始化时设置的

元素定位

继续​​Force Step Into​​​回到​​putVal​​​,此时数组已经创建好了,长度​​n​​​=16
深入源码分析HashMap到底是怎样将元素put进去的_hashmap_19
下一步​​​if ((p = tab[i = (n - 1) & hash]) == null)​​​这拆开来看,先看​​(n - 1) & hash​​,这是用数组的长度&上key的hash,得到的结果作为该键值对在数组中的位置,这个值是多少呢,我们可以自己算一下
这里的​​​&​​​是计算机的二进制按位与,n-1=15,二进制为1111,key的hash值为​​95458899​​​,这个二进制是多少,用 ​​工具​​​ 转换一下
深入源码分析HashMap到底是怎样将元素put进去的_hashmap_20
按位​​​&​​​,有0为0,全是1才是1,出来的结果是多少,是在不行也用 ​​工具​​​ 吧
深入源码分析HashMap到底是怎样将元素put进去的_构造函数_21
我们计算的结果是3,看看源码计算的结果,果然也是3
深入源码分析HashMap到底是怎样将元素put进去的_hashmap_22
那么,由于这是第一个元素,3号位之前肯定是空的,即该元素将放置在3这个位置,放一个什么进去呢
​​​newNode(hash, key, value, null)​​​深入源码分析HashMap到底是怎样将元素put进去的_后端_23
最终就是一个这个​​Node​深入源码分析HashMap到底是怎样将元素put进去的_java_24
示意图
深入源码分析HashMap到底是怎样将元素put进去的_java_25
我们继续调试,执行完放入元素之后,​​modCount​​自增,​​size​​自增,并和扩容阈值(当前是12)比较
深入源码分析HashMap到底是怎样将元素put进去的_后端_26
这个,​​modCount​​不展开讲,简单提一下,它是用在线程迭代集合时,如果发这个值变动了,和当前的值不一样,说明有人操作该集合,改变了内部数据,就会报出一个​​ConcurrentModificationException​​异常

完成

至此,无参构造,插入一个元素就完成了
深入源码分析HashMap到底是怎样将元素put进去的_构造函数_27





下回入口

下回我们再分析有参构造函数的初始化,和元素增多导致的扩容,元素重新定位问题

为了写这玩意,死了我多少脑细胞



喝瓶八二年的矿泉水,不过分吧?我自己去买,你自觉把单买了






举报

相关推荐

0 条评论