Java集合框架(四)

在这里插入图片描述

我们来看看他的注释

在这里插入图片描述

原来这个方法是跟构造函数和伪构造函数挂钩的,所以构造函数之所以没有调用super来构造继承的AbstractMap是因为调用了init方法吗?

[](()总结构造方法
  • 作者不想让我们使用自定义的容量和负载因子

  • 默认的容量为16,负载因子为0.75

  • 无参构造方法就是使用默认参数来调用有参构造方法

  • 构造方法并没有马上去给table数组设置容量,而是将阈值设为了容量

[](()put方法

在这里插入图片描述

我们来看看方法上的注释

在这里插入图片描述

从注释上就可以得知,当再次put相同key时会发生value的替换

从构造方法上可以知道,构造方法并没有立即给底层空数组立即扩上默认的容量,而是将门限值设为了默认容量

所以在put方法的开头,判断操作的table是不是EMPTY_TABLE,即针对第一次插入要对底层数组进行操作

在这里插入图片描述

调用inflateTable方法进行操作,这个参数是使用threshold作为参数的,也就是作为了下面的toSize,

[](()inflateTable

下面来看看inflateTable方法做了什么

在这里插入图片描述

步骤如下

  • 找到指定容量最近的一个2的幂次方数(要大于等于指定容量),作为真正的容量

  • 然后对threshold进行计算,取最大的容量+1和容量乘于负载因子的最小值

  • 然后重新给底层主数组table赋值,容量为指定容量最近的一个2的幂次方数

  • 然后初始化hashSeed(前面提到过,这个hashSeed是关联哈希算法的)

所以,可以得知,构造方法将threshold设为指定容量,只是用来暂时存储指定的容量,底层主数组真正有容量是在第一次put之后

接下来我们看看怎么获取最近的一个2的幂次方数(该2的幂次方数大于等于指定容量)的,也就是roundUpToPowerOf2方法

在这 **Java开源项目【ali1024.coding.net/public/P7/Java/git】** 里插入图片描述

步骤如下

  • 比较指定容量是否大于等于最大允许的容量

  • 如果大于指定容量就取最大容量( 2 30 2^{30} 230)

  • 如果小于指定容量就再判断指定容量是否大于1

  • 如果小于等于1,容量就取1(1也是2的幂次方,为零次幂)

  • 如果大于1,那么就调用Integer的highestOneBit方法

Integer的highestOneBit方法是用来获取指定参数的最小2次幂

现在我们分析一下,为什么要取指定容量减一的两倍的最小2次幂,也就是为什么参数为(nuber-1) << 1

前面已经提过,设置的容量是最近的大于或等于指定容量的2次幂,我们就围绕他来论证

2次幂,也就是0、1、2、4、8、16。。。。。这些二次幂数会形成一个个区间,如同下面所示

在这里插入图片描述

那么指定容量就会出现两种情况

  1. 指定容量在区间内

  2. 指定容量就是一个2次幂

当指定容量在区间内时,代表这个容量大于左边的二次幂数,小于右边的二次幂数,因此就有这样一个表达式 2 ( n − 1 ) < x < 2 n 2^{(n-1)} < x < 2^n 2(n−1)<x<2n(x代表容量),那么我们对条表达式进行乘2变成 2 n < 2 x < 2 ( n + 1 ) 2^n < 2x < 2^{(n+1)} 2n<2x<2(n+1),所以2倍的容量的最近的小二次幂就是容量的最近的大二次幂

当指定容量就是一个2次幂,也就是不落在上述的区间内,那么就需要进行减1,来让其落在区间内(而且这个减1操作对于本就在区间内的数没有影响),当然,比如2和1,如果指定容量为2,那么减一就变成了1,但这里要注意的是必须要大于1,才会执行这个方法,所以我们不用考虑1的情况

至于Integer的highestOneBit方法的细节,能力不足,看不懂,这里就贴上源码自己看吧

在这里插入图片描述

最后就是进行initHashSeedAsNeeded,这个方法是用来使用代替散列才使用的, 不太重要,因为我们一般使用hashMap都不会使用字符串来代替散列

inflateTable方法到此结束

总结一下inflateTable干了什么东西

  • 确定容量,容量大小为指定容量的最近的大二次幂

  • 修改扩容门限,扩容门限为扩容因子乘上容量

  • 如果要使用代替散列,对hashseed变量进行修改

[](()添加Key不为null元素

先贴上代码

在这里插入图片描述

我们将其分为两部分

  • 第一部分是形成链表时独有的操作

  • 第二部分是没有形成链表和形成链表时都有的操作

第一个部分步骤如下

  1. 先判断要添加的键值对的key是否为null,如果为空,就调用putForNullKey方法

  2. 如果key不为空,调用hash方法来计算key的hash值

  3. 调用indexFor来找到key的hash值对应的索引

  4. 最后根据索引找到位置,判断这个位置是否已经有键值对

  5. 如果有,代表整个位置形成了之前发生过哈希冲突,形成了链表,就需要遍历这个链表看是否出现相同的键

  6. 判断相同的键是先根据链表中的键值对的hash值是否与当前要添加的键值对相等,如果不相等,再比较key是不是相同,判断是否是相同的key是根据是不是同一个与equals方法进行综合考虑的

  7. 如果出现相同的键,那么就记录旧的键值对的value,将旧的键值对的value修改成新的value(也就是键不变,值替换)

  8. 调用recordAccess方法

  9. 最后将旧的value返回

第二个部分步骤如下

  • 让modCount自增(之前研究ArrayList也有这个变量,是用来记录年代的,也就是这个底层数组的版本变化)

  • 最后调用addEntry方法,参数为哈希值,键,值,索引

总体的架构就是如上,现在来看调用的各个方法

hash方法

先来看看hash值是怎么计算的

在这里插入图片描述

上面那部分不用管,是替代散列时使用的(使用字符串替代散列值)

步骤如下

  1. 先调用key自己拥有的hashCode方法

  2. 最后使用一序列的扰乱函数

所以键值对的hash值生成是跟key有关的,而且细节来说是跟key的hashCode有关(这也就是为什么使用集合来存储对象时,一定要重写hashCode方法,而且hashCode要 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源 跟对象的属性值有关,这样就可以将相同属性值的对象视为同一个对象)

接下来我们看看扰乱函数是干什么的,从注释来看

在这里插入图片描述

总的来说这个扰乱函数就是用来减少哈希冲突

indexFor方法

这个方法就是用来根据哈希值寻找到底层主数组的索引的

在这里插入图片描述

可以看到这个方法单纯是用哈希值与上了底层数组的最大索引(也就是长度减一)

这里是利用了与运算的特点,前面已经提到过,底层数组的长度一定是2的幂次方,那么减一操作就会让其变成全为1的二进制数,也就确保了底层数组的索引数量化成二进制里面所有数字一定全部都为1

那么此时这个算法就会等效成用哈希值对最大索引进行取余,也就是h % ( length - 1 )

所以索引的计算方法其实就是哈希值对最大索引进行取余操作,保证不会超过底层数组的最大索引

这也是为什么长度必须为2的幂次方

recordAccess

在这里插入图片描述

可以看到这个方法什么都没干,可能是为了以后方便拓展

我们再看下注释

在这里插入图片描述

注释上提到这个方法是出现重复key的时候都会调用

addEntry

最后我们再看看addEntry的方法

在这里插入图片描述

我们可以看到,addEntry方法里面竟然包含了扩容操作即resize方法

步骤如下

  • 判断当前HashMap的元素数量是否大于门限

  • 如果大于,再进行比较当前要添加键值对的位置是否为空

  • 如果也为空,那就会进行扩容操作

  • 所以,要扩容必须满足两个条件

  • 一个是HashMap的元素数量大于门限值

  • 另一个是当前添加键值对的索引位置必须发生冲突

  • 下面进行扩容,扩容调用的是resize方法,而且传的参数是当前底层数组长度的两倍

  • 扩容了之后,首先进行再次hash,即重新计算当前键值对的key的hash值

  • 然后再用新的hash值计算出新的索引位置

  • 最后就是调用createEntry方法来正式添加元素

[](()扩容

我们来看看resize是如何进行扩容的

在这里插入图片描述

可以看到,扩容操作也是会进行判断

  • 先判断旧的容量,也就是当前主数组的长度,是否等于最大容量

  • 如果已经等于,就不可以继续扩了

  • 然后将扩容门限设为Integer的最大值

  • 结束

  • 如果主数组的长度,不等于最大长度,那么就代表一定小于最大容量

  • 那么就会创建一个新容量的数组(前面已经知道容量为旧容量的两倍)

  • 然后调用transfer方法

  • 让底层的table数组指向新容量的数组

  • 最后就是重置了门限值,让门限值为最大的容量+1和新的容量乘上负载因子的最小值

可以看到,是不是少了就是将旧的数据放在新数组上的步骤,所以,可以推测到,这一步是放在了transfer上去完成的

在这里插入图片描述

可以看到,对于旧数组,不是像ArrayList那样单纯的进行复制过去,而是要重新散列的

可以看到,使用了增强for来桥套while循环,来遍历所有元素(增强for遍历数组,while循环来遍历每个项的链表),然后对所有键值对的hash(键值对不仅保存键值,还保存着哈希值和下一个键值对的指向)进行了重新计算,这一步称为rehash,当然,计算的规则依然是使用key来计算的,重新计算了hash当然也要重新计算索引,然后根据重新计算的索引,对应存放在新创建的扩容数组里面

不过这里有一个问题?如果rehash了之后重新装填又发生了哈希冲突会怎样?

根据源码来看,如果重新装填的过程发生了哈希冲突,那么后面的键值对就会替代前面的键值对

至此,整个扩容过程就结束了

总结一下

  • 扩容要先判断旧的容量是否已经最大,最大的话,只是单纯改变了门限值为Integer最大值

  • 扩容后的容量为旧的容量的2倍

  • 扩容后要将旧的数据重新装填进新的数组里面,重新装填并不是单纯的复制,而是要遍历所有项的链表进行重新hash,重新计算索引值,然后根据新的索引值放在数组里面

  • 假如重新装填过程中发生哈希冲突,产生的索引值一样,那么就会进行数据替换,会产生数据丢失

  • 最后将底层table指向新扩容后的数组

  • 将门限改为新的容量乘上负载因子

[](()createEntry

最后我们来看看createEntry方法

在这里插入图片描述

这个方法也就是真正底层的添加操作

  1. 首先获取底层数组指定索引位置处的旧键值对

  2. 让后新建一个键值对,键值对存要添加的键值、哈希值和指向下一个的旧键值对

在这里插入图片描述

可以知道,底层的插入使用的是一个头插法

[](()补充:当添加的是key为null的元素

前面put方法源码上,是会判断key是否为null的,假如为null就会执行putForNullKey的方法

这样拆开是因为hash值是根据key的hashCode来生成的,如果key为null就不能生成hashCode,所以需要进行拆开

在这里插入图片描述

可以判断,putForNullKey的执行跟put方法后面的操作差不多

可以看到for循环里面,遍历的是底层table数组的第一个项,也就是第一个项的链表

那么就可以知道,key为null的键值对都是放置在底层table数组第一个项里面来形成链表的,当然里面还存在key不为null的键值对对象,然后如果发现有key也为null,而且value相等的,就会发生value替换,返回旧的value

可以看到,addEntry里面的bucketIndex变为了0,hash值也变成了0

所以,key为null的元素的hash为0,即使判断要进行扩容,重新hash的时候,因为key为null,索引也是为0的,因为bucketIndex为0,所以是存储在底层table数组里面的第一个项里面

[](()get方法

接下来看看get方法是如何执行的

在这里插入图片描述

步骤如下

  • 先判断key是否为null

  • 如果为null就要在执行getForNullKey方法

  • 如果Key不为null,执行getEntry方法

  • 然后判断是否找到

  • 通过比对获得的entry是否为Null来判断

  • 如果为null就返回null

  • 如果不为null,就返回entry的value属性

[](()getEntry

我们先来看看如果key不为null时执行的getEntry方法

在这里插入图片描述

步骤如下

  • 先判断HashMap里面的元素数量是否为0,如果为0就直接返回null

  • 如果不为0,就要根据给的key计算哈希值

Java核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

image

image

image

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补
FFF,t_70#pic_center)

步骤如下

  • 先判断HashMap里面的元素数量是否为0,如果为0就直接返回null

  • 如果不为0,就要根据给的key计算哈希值

Java核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

[外链图片转存中…(img-II01UbAQ-1650011560200)]

[外链图片转存中…(img-3aebDk3I-1650011560201)]

[外链图片转存中…(img-pRRgGaiv-1650011560201)]

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值