生动有趣带你看JDK1.8-HashMap的put源码,看完直呼原来这么简单!

本博客不会大段大段的贴源码,这样谁看了都头疼,会一步一步引导你的思路,然后一点一点展示源码,让你理解原来是这样的感悟!那就开始把!

在说HashMap之前来梳理一下基本知识,如果我说数组和链表,你头脑中的数组什么样子的?如果是以下这个样子你能接受就往下走!

那么你觉得链表是什么样子的呢?如果你觉得是以下的样子能接受,那么继续跟着思路往下走!

因为HashMap就是数组+链表/红黑树的形式,所以下图这样表示你应该能接受!

那么接下来再说一下HashMap类中的部分成员变量。那么在HashMap数组是在哪里体现的呢?下图就是HashMap中数组定义的地方,大家自行搜索table

那么数组找到了,链表又在哪里体现呢?找到了,是在一个内部类里面叫Node的地方,单链表里存储的是数据(key,value)和指向下一个地址的指针(next),展现形式就是每个小节点表示的是一个Node类(hash,key,value, Nodenext)

上面的我们都清楚的知道了,接下来思考一个问题,数组的大小是多少呢?能接受的最大值?链表的长度呢?

找到定位数组大小的变量了,DEFAULT_INITIAL_CAPACITY进行左移操作,默认大小是16

数组的最大值

链表长度达到8就转换树

当红黑树节点达到6就变为链表

 

还有一个很重要的变量,估计面试经常被问把! 就是负载因子,再说负载因子之前咱们来想一想数组的问题,数组默认大小是16,那么不够用了怎么办?对,就是扩大,扩容,但是何时扩容呢?下图是负载因子,默认是0.75

 HashMap的负载因子是0.75,作用是用做扩容的标准,当数组长度16*0.75=12那也就是说当数组长度达到12的时候就会进行扩容,那为什么会选择0.75这个数呢,如果选择0.5,那么也就是在8的时候进行扩容,那不合适,也就是说当16长度的数组使用到8的时候就会进行扩容,显然很浪费,负载因子设置的太大,马上数组要满了才去扩容也不合适,所以最后定下0.75,也就是说负载因子是扩容的标准

 终于说道重点了,带你进入HashMap的put的源码过程了

HashMap的put的过程

再说主过程之前还得绕一下弯子,是为了更好消化源码头疼的问题,那就是我们的put的key,value到底存在哪里,数组的什么位置?所以在存储之前那设计者肯定要要得到key,value Node的位置

我们都知道数组的长度是16,按下标来说我需要直到0-15的位置,我们都知道数组的长度是16,那么我需要知道0-15的位置,我可以用random.nextInt(16)只能是1-16的数,我这样设计我也给它叫hash算法可以么,其实是有问题的,这样找位置随便放太随便了,随便的去找会出现碰撞问题。那hash算法也就没有了意义。也就是说思想还是这样的:肯定需要0-15之间的一个值

  1. 得到一个整型数字   key,value  key.hashcode
  2. 把这个整型数控制在0-15

演示一下用我的名字计算hash值,得到的值是3202,用取余法3202%16=2,也就找到了位置。

 

 但是在HashMap中进行hash不是进行取余的,看一下HashMap的hash是怎么生成的?看下图,hash算法是采用高16位和低16位做异或运算,这样做尽可能让hash算出来的数值不同,分布均匀。分布均匀的好处就是数组都能"雨露均沾" 不能一直在一个数组位置下一直无尽的加数据,那样就失去了数组的意义。

 准备工作整理完,开始正式进入源码部分,请自行打开jdk1.8源码进行比对学习。加下关注更感激不尽了,我会持续奋进学习书籍、视频,然后记录博客,让大家都能一起学习到

数组初始化前,先把table成员变量赋给了tab局部变量,然后判断数组是不是null

如果为null,就需要进行初始化操作, resize

 进入到resize里面以后发现怎么这么长呢,你先别管你就只记得我们最熟悉的看,要不一句一句的看就懵逼了,以下我标红的是不是异常的熟悉啊。下面一一讲解

首先把table变量赋给了局部变量oldtable

接下来再看,如果oldtable为null就给oldCap赋值0,不为null就把oldtable长度赋值给oldCap

判断一下oldcap长度,这里边假设第一次用长度为0,就会进入else的判断

everyBody下面这张图是不是很熟悉啊,上面截图有讲过哦,默认数组大小为16赋值给newCap;负载因子*默认数组大小赋值给newThr;哎呀呀,是不是很熟悉啊

接下来再把newThr赋值给全局变量threshold

 天哪准备了半天,折腾了半天终于初始化进行分配数组内存的操作了,闪亮登场

唉呀妈呀,初始化后面咋还这么长呢,你先别管,就看返回值,是不是返回你初始化的数组了。 

数组初始化完了,key,value     Node node=new Node(hash,key,value,next) 这些我们现在都知道了,也都有了,next咱现在不知道那就暂时为null,但是现在hash算出存储的位置了但是还不知道它这里边是怎么计算,那还是不是上边说的取模运算呢?3202%16?再跳回putVal方法里看一下。呀,没有用取模运算啊用了&(与位运算)。n=16-1,hash=3202,为什么用&运算,因为这样的效率更高。此判断是在tab不为null,之前有值存储进入。

然后位置算出来了,是不是要存储了,那存储之前我需要计算我现在算出来的位置是否被其他数据给占据了呢,

(p = tab[i = (n - 1) & hash]) == null 判断当前位置是否有别人占据了!

如果没有占据是null的,根据给的hash,key,value,next直接创建新的node节点,然后就放在当前的位置就ok了。

那么如果当前位置已经被其他元素占据了呢,是不是要做其他判断了呢,继续看else的代码,哎,又这么长,全都是判断,那么我们就先看最外面的判断,首先如果我put("1":"df"),然后又进来了put("1":"xxh"),那么会做什么?对,key一样的话要做更新替换操作,对应了如下代码,如果key相同,hash也相同就进行更新值操作

 如果我要存一个元素,但是之前已经存储了,key是不一样的,然后hash处在同一个位置,要存储的话就需要判断链表指针next是否为null,判断不为null就继续next,直到发现next是null的,那好就在next指向为null的位置进行存储,对应源码如下

for //遍历链表

这块就是查找到为null的位置就新创建节点进行存储

如果已经是红黑树的模式了,那么在新来的节点就以树的形式存储,treeNode已经是树的格式了。

注意一下这个问题,判断超过8就转换成红黑树

当数组存储的数据超过了12,就需要扩容了,源码体现在哪里呢?

你会发现之前初始化也是resize怎么扩容还是resize方法?

你会发现之前初始化也是resize怎么扩容还是resize方法

再次进入看一下,这时候经过上一些步骤,现在是有值的,此时oldCap就肯定大于0,进入判断,当oldCap超过了MAXIMUM_CAPACITY最大值也就没意义了,就不要做什么操作了。

否则进行左位移,假如之前数组是16那就扩容变成32,因为数组长度变了,计算的负载因子必然需要变化,所以也进行左位移之前是12,现在就是24了

新数组进行初始化

初始化完结构可能就会变成这样的结构,那么我怎么把原数组的东西弄过来呢?

首先判断旧数组一定有值,没值没意义

遍历以后判断当前数组位置是否有值

然后熟悉的操作,判断当前节点的下一个位置是否有数据,没有就进行hash计算,把当前节点数据存储到算出来的位置中。

如果是红黑树进行分割一块一块的进行存储

数组说了,红黑树说了,接下来想也能想到说的是哪种情况了,对的是链表情况,你会发现链表又非常长,那就看重点,看重点,看重点。。。

这个判断是hash完与上原数组长度如果等于0就存储原位置保持不动。

当前节点下标+原数组位置就存储新的位置!

Put总结:

(1)先进行hash得到一个数值,目的后续进行计算得出存储位置

(2)发现数组没有初始化,进行初始化

(3)那现在有了数组就进行存储被,怎么存储呢

3.1 计算节点的一个位置

3.2 判断计算结果当前位置是否有元素,没有的话占下当前位置

3.3 如果当前位置被占据,那是不是需要做其他判断了呢

  3.3.1 key值相同,仅仅替换value值就可以了

  3.3.2 key值不相同,如果是链表存储形式,那么就循环遍历,伺机找next=null,然后进行添加操作。

  3.3.3 key值不相同,如果是红黑树存储,那么就按照红黑树的存储形式

 

1. HashMap的原理,内存存储结构

  1. 底层使用哈希表(数组+链表/红黑树),当链表过长会将链表转成红黑树以事先O(logn)时间复杂度
  1. 讲一下HashMap中的put方法过程?
  1. 对key求hash值,然后再计算下标
  2. 如果没有碰撞直接放入桶中
  3. 如果碰撞了,以链表的方式链接到后面
  4. 如果链表长度超过阈值,就把链表转换成红黑树
  5. 如果存储时节点已经存在并重复就替换旧值
  6. 如果桶满了(容量*负载因子),就需要扩容
  1. HashMap中hash函数怎么实现的?还有哪些hash的实现方式?
  1. 高16bit不变,低16bit和高16bit做了一个异或
  2. (n-1)&hash—>得到下标
  1. HashMap怎样解决冲突,讲一下扩容过程,假如一个值再原数组中,现在移动了新数组,位置肯定改变了,那是什么定位到这个值新数组中的位置?
  1. 将新节点加到链表后
  2. 容量扩充原来的两倍,然后对每个节点重新hash值
  3. 这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原数组>的位置

本博客根据咕泡学院的-jack老师的HashMap源码视频进行整理总结,码字不易,给个赞再走被!希望本博客能够帮助你!

加下关注更感激不尽了,我会持续奋进学习书籍、视频,然后记录博客,让大家都能一起学习到!笔心

猜你喜欢

转载自blog.csdn.net/dfBeautifulLive/article/details/105249021