HashMap中扩容问题夺命6连问,问到了硬件层,你能顶住吗?

简介: HashMap中扩容问题夺命6连问,问到了硬件层,你能顶住吗?
                            ✨ 我是喜欢分享知识、喜欢写博客的YuShiwen,与大家一起学习,共同成长!
                                      📢 闻到有先后,学到了就是自己的,大家加油!
📢 导读:
文章不长,看完后相信你一定会有所收获,本篇文章和一般的面试文章不一样,本篇文章知识点更细、更深,直接到硬件层,更进一步拓宽读者对于计算机的认知,并且还力求知其所以然!
                                

1.HashMap中扩容为什么是2的n次幂

答:

源码是这样写的,扩容时把当前hash表的数组长度左移一位,即乘以2;在计算数组下标的时候,还用到了2的n次幂。

2.如何用到了2的n次幂?

在这里插入图片描述

答:在计算数组下标的时候,先把Hash表中数组的长度减1,然后在与key的hash值进行按位与(&)操作.

即i=(n-1)&hash;其中i是tab数组的下标,n是Hash表中数组的长度,hash是key通过hashCode方法计算得到的值,源码如下:
在这里插入图片描述



3.如果不是2的n次幂会出现什么情况?

答:我们用反推法证明一下,如果不是2的n次幂,会怎么样,具体内容如下:

在这里插入图片描述
i=(n-1)&hash;其中i是tab数组的下标,n是Hash表中数组的长度,hash是key通过hashCode方法计算得到的值,源码如上:

这里笔者就拿刚开始的扩容量来说,之前是2的n次幂的时候,初始化长度是16,如果不是2的n次幂,我们就举例为13,那么执行上面这段代码的时候,会出现这样的现象:(备注:int类型有32为,为节约空间,这里我只写出低8位,剩下的24位没写出的全是0)

首先13的二进制位0000 1101;执行n-1,二进制结果为0000 1100;执行的结果0000 1100与任意的hash进行&操作时,得到的值中第1位和第2位永远是0,即得到的值只能为以下四种可能:
0000 0000
0000 0100
0000 1000
0000 1100

也就是说初始化长度位13的hash表数组,其中只有下标为0、4、8、12的能存放值,下标为1、2、3、5、6、7、9、10、11是没有用到的,如下图:
在这里插入图片描述
这就会导致让节点成为超长链表的概率大大增加,导致之后的查询时间过长。

如果是2的n次幂,同样的我们执行上述三个步骤:

首先16的二进制位0001 0000;执行n-1,二进制结果为0000 1111;执行的结果0000 1111与任意的hash进行&操作时,得到的值的范围为0-12,包括了数组的全部,也就是说都利用上了。

图就变成了下图:

在这里插入图片描述



4.在2中你提到了&(与)操作,为什么HashMap源码里面不用取余或取模来替代&(与)操作?

答:因为取余或取模计算结果有负数,最重要的是在运算速度上,取余或取模操作没有&(与)操作快。



5.那为什么取余或取模计算结果有负数?为什么取余或取模操作没有&(与)操作快?

内心活动:提问者一直在问这个问题,而且都问得这么底层了,这里就不能简单一句话就回答了,想考验我底层功底,那就且听我慢慢道来。

5.1取余或取模计算结果有负数的原因如下:

这里说一下取模Math.floorMod和取余%的区别,在Java中取模的定义如下:
在这里插入图片描述
因为取余运算是操作符%,Java中看不到源码,笔者参考数学中对于取余的定义,大致内容如下:

public static int fixMod(int x, int y){
     
    int r = x - fixDiv(x,y)*y;
    return r;
}

floorMod调用了floorDiv方法,fixMod调用了fixDiv方法,floorDiv方法和fixDiv都是求商的运算;
其中floorDiv是向负无穷取整,fixDiv是向0取整:

当是正数时,比如3/5,floorDiv(商)和fixDiv(商)的值是相同的,都是0;当是负数时,比如-3/5,floorDiv(商)向负无穷取整为-1,fixDiv(商)向0取整为0。

总结就是:

取余,遵循尽可能让商,即fixDiv方法向0靠近的原则;取模,遵循尽可能让商,即floorDiv方法向负无穷靠近的原则

上面是取余与取模运算其中的一个区别,接下我们看另外一个区别,这个也是重点,我们看一个例子:
在这里插入图片描述
上例代码如下,大家可以自行复制粘贴运行一下康康:

println "5,3取模运算为:"+Math.floorMod(5,3);
println "5,3取余运算为:"+5%3;
println "";
println "-5,-3取模运算为:"+Math.floorMod(-5,-3);
println "-5,-3取余运算为:"+(-5%-3);
println "";
println "5,-3取模运算为:"+Math.floorMod(5,-3);
println "5,-3取余运算为:"+(5%-3);
println "";
println "-5,3取模运算为:"+Math.floorMod(-5,3);
println "-5,3取余运算为:"+(-5%3);

运行结果如下:
在这里插入图片描述
有如下情况:

当除数和被除数符号相同时,结果相同;当除数和被除数符号不相同时,有如下两种情况: 为取模运算时,运算结果的符号和除数相同;为取余运算时,运算结果的符号和被除数相同。

通过上面可以可以得知,不论是取模运算还是取余运算,当除数或被除数是负数时,那么计算结果也可能为负数。

5.2取余或取模操作没有&(与)操作快的原因如下:

普通计算器是通过硬件的逻辑运算实现加减乘除的:

从数学上讲,CPU中的ALU在算术上只干了三件事,加法,移位,取反;
在实际的物理电路中,只有与、或、非、异或这四种门电路;
知道了这两点,我们再来分析加减乘除:

加法:逻辑上异或操作,即0与0和1与1为0,0与1和1与0为1,得到本位和的值,根据运算要求,确定是否要进位;减法:对于计算机来说没有减法,减法是把某一个数看做负数,然后在计算机中用补码存起来(即原码取反加1),然后执行加法运算;(加法也是用的补码计算的)乘法:移位,逻辑判断,累加;除法:移位,逻辑判断,累减。

2位加法器门电路大致如下:(这里以两位加法器举例,现实中可以由更多的这些门电路组成更多位的加法器)
在这里插入图片描述
在这里插入图片描述
乘法器门电路图如下,乘法器由加法器和与门组成(同样这里也只举例2位*2位的乘法,更高位的门电路更复杂,但是实现的基本方式是没有变化的)

在这里插入图片描述

从硬件实现上讲,可以看出,实现这四种基本运算只需要加法器、移位器和基本逻辑门电路硬件组件就行。早期的cpu里结构简单,只有这些组件,没有专门的乘法器、除法器。后来的cpu会集成专门的并行处理电路在cpu内建的协处理器(比如浮点运算器,很早的cpu是没有专门计算浮点的电路的)中,在硬件上实现,这样计算速度就快了。当然,计算的逻辑还是没变。

上面3.1章节中,我们提到过,取模操作如下(取余操作也是类似的):

在这里插入图片描述

r=x-floorDiv(x,y)*y

在这个关系式中,fixMod它是求商操作,求商操作包含了除法操作,得到的商又和y相乘,上面我们讲到了:
乘法可以拆分为:移位,逻辑判断,累加;
除法可以拆分为:移位,逻辑判断,累减。
可以看到乘法和除法的底层实现就是移位操作还有累加操作,再加上一些逻辑判断,这和&(与)操作相比效率低太多了,与操作只需要一个与门逻辑电路就可以计算完成。



附录:和笔者一起看源码

测试代码(JDK1.8):
创建一个HashMap,此时为空,首先我们向HashMap中插入key=”name“,value=”YuShiwen“的键值对。
在这里插入图片描述
跟进代码中,调用了HashMap中的putVal()方法
在这里插入图片描述
跟进putVal方法中:

第一张截图:刚开始的时候Hash表是空的,即tab是null进入if语句中,调用resize()方法进行初始化,初始化长度为16。(ps:初始化或者扩容的时候会调用resize()方法,扩容的方法为左移一位,即在原来的大小上乘以2)第二张截图:第一次的时候,在resize()方法中返回默认的大小为16.
在这里插入图片描述
在这里插入图片描述

putVal方法返回默认值16后,我们继续看,(此时HashMap表的长度就是16了,即n的值是16;我们都知道HashMap底层的实现是数组+链表,即是如下第一张图的结构;)之后到了一个if语句,该if语句中就是上面提到的计算数组下标的过程,即先把Hash表中数组的长度减1,然后在与key的hash值进行按位与(&)操作。
在这里插入图片描述
在这里插入图片描述

我是喜欢分享知识、喜欢写博客的YuShiwen,与大家一起学习,共同成长!咋们下篇博文见。

已完结
于CSDN
2022.03.16
author:YuShiwen
目录
相关文章
|
6月前
|
存储 Java
HashMap扩容机制详解
HashMap扩容机制详解
|
存储 算法 Java
HashMap 之底层数据结构和扩容机制
HashMap 之底层数据结构和扩容机制
914 1
|
存储 容器
HashMap什么时候扩容,如何扩容?怎么轻松化解?
一位2年工作经验的小伙伴面试时被问到,说,HashMap什么时候扩容,为什么要扩容?这个问题本身不是很难,但是这位小伙伴对底层实现原理没有太多关注,所以,被这个问题难住了。 下面我给大家分析一下这个问题的底层逻辑。
147 2
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
64 3
|
1月前
|
算法 索引
HashMap扩容时的rehash方法中(e.hash & oldCap) == 0算法推导
HashMap在扩容时,会创建一个新数组,并将旧数组中的数据迁移过去。通过(e.hash & oldCap)是否等于0,数据被巧妙地分为两类:一类保持原有索引位置,另一类索引位置增加旧数组长度。此过程确保了数据均匀分布,提高了查询效率。
38 2
|
6月前
|
存储 安全 算法
【JAVA】HashMap扩容性能影响及优化策略
【JAVA】HashMap扩容性能影响及优化策略
|
5月前
|
存储 安全 Java
深入解析Java HashMap的高性能扩容机制与树化优化
深入解析Java HashMap的高性能扩容机制与树化优化
146 1
|
5月前
|
索引
HashMap扩容为什么每次都是之前的2倍
HashMap扩容为什么每次都是之前的2倍
85 0
|
存储 安全 Java
HashMap底层结构、扩容机制实战探索
HashMap底层结构、扩容机制实战探索
HashMap底层结构、扩容机制实战探索