深入源码分析HashMap到底是怎样将元素put进去的
说明
此次深入源码解剖是为了搞明白几个问题
1、HashMap是如何初始化的
2、HashMap的扩容机制是怎样的
3、元素是如何put
进HashMap的,具体位置在哪(重难点)
4、扩容后,元素是如何重新分布的(重难点)
注:为了方便读者复盘,我截取源码时会将源码行号也带上。
jdk版本:1.8
在深入源码之前,应该先有个大致的了解,在JDK8里面,HashMap的底层数据结构已经变为数组+链表+红黑树的结构
HashMap结构示意图
数组就是源码中的table
链表就是内部类Node
红黑树就是内部类TreeNode
解剖思路
先创建一个无参构造函数,往其内添加一个元素,看看HashMap是如何初始化的,然后再创建一个有参构造函数,并往其中添加若干元素,直至触发扩容机制
无参构造函数
在无参构造函数之前,打个断点,以debug
模式启动,关于调试键的使用请参照:IDEA调试键的说明,在此不再赘诉。
点击Step Over
到HashMap map = new HashMap();
所在的这行
点击Force Step Into
,会发现先调用的是类加载器
这不是今天的重点,我们直接Step Out
,然后再次点击Force Step Into
,进入源码
初始化负载因子
这行代码的意思是loadFactor
设置为默认的负载因子 0.75,上面的因为注释说同时初始容量为16,这里暂时没看到在哪了赋值了
反倒是你在475
行点击Force Step Into
会发现它调用的父类的构造函数
这也补充一个知识点,有父类构造函数的先执行父类的构造函数,再执行自己的构造函数
执行完父类的构造函数,将负载因子设置为默认,初始化就此完成了
继续点击Force Step Into
,来到map.put("debug","HashMap");
继续点击Force Step Into
,进入源码611
行
计算key的hash值
继续点击Force Step Into
来到计算key的hash值
继续点击Force Step Into
会发现因为key的类型是String,最终调用的是String的hashCode,经过计算后key的hash值为95458899
继续点击Force Step Into
,算完key的hash值,然后调用putVul
初始扩容
因为是刚刚构造的所以tab
为null
,所以要走一波resize()
点击Force Step Into
进去看看,用红线框出执行路径
因为oldTab
是为null
的所以直接返回newTab
回顾一下这段源码,虽然很长很复杂,但对于此次调试,核心的就是以下三行
将容量设置为默认容量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
下一步if ((p = tab[i = (n - 1) & hash]) == null)
这拆开来看,先看(n - 1) & hash
,这是用数组的长度&上key的hash,得到的结果作为该键值对在数组中的位置,这个值是多少呢,我们可以自己算一下
这里的&
是计算机的二进制按位与,n-1=15,二进制为1111,key的hash值为95458899
,这个二进制是多少,用 工具 转换一下
按位&
,有0为0,全是1才是1,出来的结果是多少,是在不行也用 工具 吧
我们计算的结果是3,看看源码计算的结果,果然也是3
那么,由于这是第一个元素,3号位之前肯定是空的,即该元素将放置在3这个位置,放一个什么进去呢newNode(hash, key, value, null)
最终就是一个这个Node
示意图
我们继续调试,执行完放入元素之后,modCount
自增,size
自增,并和扩容阈值(当前是12)比较
这个,modCount
不展开讲,简单提一下,它是用在线程迭代集合时,如果发这个值变动了,和当前的值不一样,说明有人操作该集合,改变了内部数据,就会报出一个ConcurrentModificationException
异常
完成
至此,无参构造,插入一个元素就完成了
下回
下回我们再分析有参构造函数的初始化,和元素增多导致的扩容,元素重新定位问题