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

说明

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

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

jdk版本:1.8

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

HashMap结构示意图

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

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

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

解剖思路

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

无参构造函数

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

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

点击Step OverHashMap map = new HashMap();所在的这行

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

初始化负载因子

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

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

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

计算key的hash值

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

初始扩容

因为是刚刚构造的所以tabnull,所以要走一波resize()
深入源码分析HashMap到底是怎样将元素put进去的
点击Force Step Into进去看看,用红线框出执行路径
深入源码分析HashMap到底是怎样将元素put进去的
因为oldTab是为null的所以直接返回newTab
深入源码分析HashMap到底是怎样将元素put进去的
回顾一下这段源码,虽然很长很复杂,但对于此次调试,核心的就是以下三行
深入源码分析HashMap到底是怎样将元素put进去的
将容量设置为默认容量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进去的
下一步if ((p = tab[i = (n - 1) & hash]) == null)这拆开来看,先看(n - 1) & hash,这是用数组的长度&上key的hash,得到的结果作为该键值对在数组中的位置,这个值是多少呢,我们可以自己算一下
这里的&是计算机的二进制按位与,n-1=15,二进制为1111,key的hash值为95458899,这个二进制是多少,用 工具 转换一下
深入源码分析HashMap到底是怎样将元素put进去的
按位&,有0为0,全是1才是1,出来的结果是多少,是在不行也用 工具
深入源码分析HashMap到底是怎样将元素put进去的
我们计算的结果是3,看看源码计算的结果,果然也是3
深入源码分析HashMap到底是怎样将元素put进去的
那么,由于这是第一个元素,3号位之前肯定是空的,即该元素将放置在3这个位置,放一个什么进去呢
newNode(hash, key, value, null)
深入源码分析HashMap到底是怎样将元素put进去的
最终就是一个这个Node
深入源码分析HashMap到底是怎样将元素put进去的
示意图
深入源码分析HashMap到底是怎样将元素put进去的
我们继续调试,执行完放入元素之后,modCount自增,size自增,并和扩容阈值(当前是12)比较
深入源码分析HashMap到底是怎样将元素put进去的
这个,modCount不展开讲,简单提一下,它是用在线程迭代集合时,如果发这个值变动了,和当前的值不一样,说明有人操作该集合,改变了内部数据,就会报出一个ConcurrentModificationException异常

完成

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

下回

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