对HashMap源码中put方法的理解

目录

HashMap的底层简单阐述

来看看jdk8以后的HashMap的部分源码

数据结构

 hash()求哈希值的方法

Put()和putVal()方法

put方法

 putVal方法


HashMap的底层简单阐述

我们都知道HashMap的底层:

jdk8之前是单纯的数组+链表,新元素放在数组上,老元素挂在新元素下面

jdk8以后数组+链表+红黑树实现的,红黑树是在链表长度>=8同时数组长度>64之后出现,会将数组下面挂着的链表(>8的)转为红黑树,新元素直接挂在老元素下面(区别于jdk8之前)

来看看jdk8以后的HashMap的部分源码

数据结构

我们可以看到,HashMap内部创建了一个内部类Node(实现了Map.Entry接口),用于存储键值对(Entry)对象,成员如下:

hash:键的哈希值

key:键

value:值

Node<K,V> next:下一个节点的地址

 hash()求哈希值的方法

我们都知道hashMap的哈希值只和键有关,从源码就可以看出来,形参只有键Key,用Key求得最后的哈希值,通过键Key调用Key类内部的hashCode方法,这里是直接地址作哈希(默认),所以当我们的键是自定义类对象的时候,我们要在自定义类内部重写equals和hashCode方法,比如:

Put()和putVal()方法

put方法

 这里可以看到put方法其实就是调用了putVal方法具体的添加节点的实现在putVal方法中

注意到这里的onlyIfAbsent和evict形参分别是false和true,此外调用了hash(key)方法传入了哈希值给putVal()

  • onlyIfAbsent:一个布尔值参数,如果为true,表示只有在键不存在的情况下才将新的键值对添加到Map中,否则如果键已经存在,就不更新其对应的值。如果为false,则不论键是否已存在,都将新的键值对添加到Map中,也就是存在就覆盖,不存在就添加。

  • evict:一个布尔值参数,主要是为了给LinkedHashMap中做兼容操作,在HashMap中没有实际意义

 putVal方法

源码大致如下:

接下来我来一行一行解释:

首先新建了数组tab作为第三方接收Node方法中的table,这里的操作是为了提高效率,节省开销:我们知道实例数组是在堆内存中存储的,如果我们直接用Node类中自己的table的话,每次要从方法(存在栈内存中)到堆中访问,开销会较大,所以在putVal方法中创建一个局部的tab指向table

 这里if是给让tab指向table数组,也就是哈希表中的数组,如果这个是个空数组或者数组长度为0,让tab调用resize,也就是对数组扩容,我们来看看resize方法部分源码

 

 这里的threshold其实就是扩容阈值,一般都是capacity*loadFactor:加载因子乘以数组长度

这里因为我们在putVal中tab是空的一开始,也就是第一次从0到有的扩容,所以执行到resize中的红框标注的代码

 

 这里的大写由下划线连接的就是常量,也就是和C语言中的宏定义类似,最后得到

newCap=16;(初始容量是16

newThr=0.75f*16=12;(扩容阈值一开始是12,也就是达到12才继续扩容)

所以回到putVal源码,也就是这个if分支,新建了一个容量为16键值对的新数组tab

 这一个if就比较简单,就是通过传入putVal的哈希值hash,计算出键值对应该在数组中的位置(索引 i),并且用Node对象p先存着这个索引位置的结点

(索引index=(数组长度-1)&哈希值)

这里的数组长度-1就是为了保证得到的索引合法,&表示按位与操作

如果p=null,也就是说这个位置是空的,就直接在这个索引位置添加这个节点

 接下来进入else,也就是该索引位置不是null(p!=null),也就是已经有节点了

继续判断我要添加的这个节点 是不是 完全等于(哈希值hash和键key相等就行)索引位置的p节点

如果相等,就用e存储p(后面有用);

进入else if分支,也就是我要添加的节点的哈希值,仅仅是和p的哈希值,发生了哈希冲突,并不是一模一样的键 (hash同,键不同),并且p的类型还是红黑树中的节点类型虽然我们是用Node类型创建的p对象,但是TreeNode是Node子类,多态的性质(可以参考我的另一篇文章:
多态的简单介绍),所以p=tab[i]的时候TreeNode可以传给p
),就调用红黑树的添加方法,加入p位置下面的红黑树中

 

接着看下一个else,这里意思就是,它既不是和p同hash同键的节点,也不是红黑树节点,所以只剩下一种情况:链表节点

这里使用的是for循环遍历链表,我们要做的就是在链表的末端添加上我们的节点,用e=p.next存放下一个节点,然后p=e继续推进链表的遍历

在循环中又有两种情况(同一索引下的链表hash一定一样):

1.链表中也没有和新结点一样(键相同)的节点

第一个if,直接进行到链表末尾,添加节点(让最后一个结点指向新节点),同时他还加了一个检测机制,也就是if(binCount>=TREEIFY_THRESHOLD-1)这里的binCount就是来记录链表长度,因为从0开始,所以和TREEIFY_THRESHOLD-1(也就是8-1)比较,实际上就是我最开始说的,链表长度>8的时候会转为红黑树存储结点,然后跳出循环

2.链表遍历过程中找到了同键的节点 

找到同键的节点,直接跳出循环,注意这时候的e就是存储的当前键相同的那个节点

 

继续看源码,注意到这里结束,才跳出大的else循环 

这一块代码的作用是对同键节点的值覆盖,仅仅是值覆盖

(其实这里就看到了形参onlyIfAbsent发挥作用了,HashMap中的put方法中给putVal传入的默认是false,也就是会进行覆盖)

比如e存储的是("zhangsan=1"),我传入的新节点是("zhangsan=2"),=左边是键,右边是值

这时候就用2覆盖了原先的值1

这个if只有当e非空才能进入 ,也就是e非空才能进行覆盖操作,我们回顾我们之前的几种情况

1.e直接和数组索引位置的p相等,实施覆盖,返回老的值Value,正好和put方法的类型Value对应

2.添加到红黑树下:e=putTreeVal的返回值,e是什么根据putTreeVal而定

3.添加到链表下:①链表中无重复节点,直接添加,e=null,不进入这个分支

                         ②有重复的,我们用e存储了该节点,实现覆盖并返回老节点的值Value

 

 跳出分支之后,说明没有进入之前的if(e!=null)分支,最后都会返回null,除此之外,最后还有扩容检测,++size判断本次添加完是不是已经达到了扩容阈值,达到了的话就提前扩容(扩容到原先的2倍(左移1位))

在HashMap中,size指的是当前哈希表中键值对结点的总数目,而capacity指的是哈希表中桶数组的长度。 

为什么这里加扩容检测呢,之前的if(e!=null)不加?

因为,e!=null是单纯的覆盖Value,并没有改变节点数目,而走到这的都是新添加了结点的情况,所以才进行扩容检测

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值