金三银四助力面试-手把手轻松读懂HashMap源码(1),BTAJ面试有关散列(哈希)表的面试题详解

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

HashMap 中是通过数组 + 链表 + 红黑树来实现数据的存储(jdk1.8),而在更早的版本中则仅仅采用了数组 + 链表的方式来进行存储。

为什么建议初始化 HashMap 时指定大小


HashMap 初始化的时候,我们通常都会建议预估一下可能大小,然后在构造 HashMap 对象的时候指定容量,这是为什么呢?要回答这个问题就让我们看一下 HashMap 是如何初始化的。

下图就是当我们不指定任何参数时创建的 HashMap 对象:

在这里插入图片描述

负载因子

可以看到有一个默认的 DEFAULT_LOAD_FACTOR(负载因子),这个值默认是 0.75。负载因子的作用就是当 HashMap 中使用的容量达到总容量大小的 0.75 时,就会进行自动扩容。然后从上面的源码可以看到,当我们不指定大小时,HashMap 并不会初始化一个容量,那么我们就可以大胆的猜测当我们调用 put 方法时肯定会有判断当前 HashMap 是否被初始化,如果没有初始化,就会先进行初始化。

HashMap 默认容量

put 方法中会判断当前 HashMap 是否被初始化,如果没有被初始化,则会调用 resize 方法进行初始化,resize 方法不仅仅用于初始化,也用于扩容,其中初始化部分主要是下图中红框部分:

在这里插入图片描述

可以看到,初始化 HashMap 时,主要是初始化了 2 个变量:一个是 newCap,表示当前 HashMap 中有多少个桶,数组的一个下标表示一个桶;一个是 newThr,主要是用于表示扩容的阈值,因为任何时候我们都不可能等到桶全部用完了才去扩容,所以要设置一个阈值,达到阈值后就开始扩容,这个阈值就是总容量乘以负载因子得到。

上面我们知道了默认的负载因子是 0.75,而默认的桶大小是 16,所以也就是当我们初始化 HashMap 而不指定大小时,当桶使用了 12 时就会自动扩容(如何扩容我们在后面分析)。扩容就会涉及到旧数据迁移,比较消耗性能,所以在能预估出 HashMap 需要存储的总元素时,我们建议是提前指定 HashMap 的容量大小,以避免扩容操作。

PS:需要注意的是,一般我们说 HashMap 中的容量都是指的有多少个桶,而每个桶能放多少个元素取决于内存,所以并不是说容量为 16 就只能存放 16key 值。

HashMap 最大容量是多少


当我们手动指定容量初始化 HashMap 时,总是会调用下面的方法进行初始化:

在这里插入图片描述

看到 453 行,当我们指定的容量大于 MAXIMUM_CAPACITY 时,会被赋值为 MAXIMUM_CAPACITY,而这个 MAXIMUM_CAPACITY 又是多少呢?

在这里插入图片描述

上图中我们看到,MAXIMUM_CAPACITY 是 2 的 30 次方,而 int 的范围是 2 的 31 次方减 1,这岂不是把范围给缩小了吗?看上面的注释可以知道,这里要保证 HashMap 的容量是 2 的 N 次幂,而 int 类型的最大正数范围是达不到 2 的 31 次幂的,所以就取了2 的 30 次幂。

我们再回到前面的带有参数的构造器,最后调用了一个 tableSizeFor 方法,这个方法的作用就是调整 HashMap 的容量大小:

在这里插入图片描述

这个方法如果大家不了解位运算,可能就会看不太明白这个到底是做什么操作,其实这个里面就做了一件事,那就是把我们传进来的指定容量大小调整为 2 的 N 次幂,所以在最开始要把我们传进去的容量减 1,就是为了统一调整。

我们来举一个简单的例子来解释一下上面的方法,位运算就涉及到二进制,所以假如我们传进来的容量是一个 5,那么转化为二进制就是 0000 0000 0000 0000 0000 0000 0000 0101,这时候我们要保证这个数是 2 的 N 次幂,那么最简单的办法就是把我们当前二进制数的最左边的 1 开始,一直到最右边,所有的位都是 1,那么得到的结果就是得到对应的 2 的 N 次幂减 1,比如我们传的 5 进来,要确保是 2 的 N 次幂,那么肯定是需要调整为 2 的 3 次 幂,即:8,这时候我么需要做的就是把后面的 3101 调整为 111 ,就可以得到 2 的 3 次幂减 1,最后的总容量再加上 1 就可以调整成为 2 的 N 次幂。

还是以 5 为例,无符号右移 1 位,得到 0000 0000 0000 0000 0000 0000 0000 0010,然后再与原值 0000 0000 0000 0000 0000 0000 0000 0101 执行 | 操作(只有两边同时为 0,结果才会为 0),就可以得到结果 0000 0000 0000 0000 0000 0000 0000 0111,也就是把第二位变成了 1,这时候不论再右移多少位,右移多少次,结果都不会变,保证了后三位为 1,而后面还要依次右移,是为了确保当一个数的第 31 位为 1 时,可以保证除了最高位之外的 31 位全部为 1

到这里,大家应该就会有疑问了,为什么要如此大费周章的来保证 HashMap 的容量,即桶的个数为 2 的 N 次幂呢?

为什么 HashMap 容量大小要为 2 的 N 次幂


之所以要确保 HashMap 的容量为 2 的 N 次幂的原因其实很简单,就是为了尽可能避免哈希分布不均匀而导致每个桶中分布的数据不均匀,从而出现某些桶中元素过多,影响到查询效率。

我们继续看一下 put 方法,下图中红框部分就是计算下标位置的算法,就是通过当前数组(HashMap 底层是采用了一个 Node 数组来存储元素)的长度 - 1 再和 hash 值进行 & 运算得到的:

在这里插入图片描述

& 运算的特点就是只有两个数都是 1 得到的结果才是 1,那么假如 n-1 转化为二进制中含有大量的 0,如 1000,那么这时候再和 hash 值去进行 & 运算,最多只有 1 这个位置是有效的,其余位置全部是 0,相当于无效,这时候发生哈希碰撞的概率会大大提升。而假如换成一个 1111 再和 hash 值进行 & 运算,那么这四位都有效参与了运算,大大降低了发生哈希碰撞的概率,这就是为什么最开始初始化的时候,会通过一系列的 | 运算来将其第一个 1 的位置之后所有元素全部修改为 1 的原因。

谈谈 HashMap 中的哈希运算


上面谈到了计算一个 key 值最终落在哪个位置时用到了一个 hash 值,那么这个 hash 值是如何得到的呢?

下图就是 HashMap 中计算 hash 值的方法:

在这里插入图片描述

我们可以看到这个计算方法很特别,它并不仅仅只是简单通过一个 hashCode 方法来得到,而是还同时将 hashCode 得到的结果无符号右移 16 位之后再进行异或运算,得到最终结果。

这么做的目的就是为了将高 16 位也参与运算,进一步避免哈希碰撞的发生。因为在 HashMap 中容量总是 2 的 N 次幂,所以如果仅仅只有低 16 位参与运算,那么有很大一部分情况其低 16 位都是 1,所以将高 16 位也参与运算可以一定程度避免哈希碰撞发生。而后面之所以会采用异或运算而不采用 &| 的原因是如果采用 & 运算会使结果偏向 0,采用 | 运算会使结果偏向 1^ 运算会使得结果能更好的保留原有特性。

put 元素流程


put 方法前面的流程上面已经提到,如果 HashMap 没有初始化,则会进行初始化,然后再判断当前 key 值的位置是否有元素,如果没有元素,则直接存进去,如果有元素,则会走下面这个分支:

在这里插入图片描述

这个 else 分支主要有 4 个逻辑:

  1. 判断当前 key 和原有 key 是否相同,如果相同,直接返回。

  2. 如果当前 key 和原有 key 不相等,则判断当前桶存储的元素是否是 TreeNode 节点,如果是则表示当前是红黑树,则按照红黑树算法进行存储。

  3. 如果当前 key 和原有 key 不相等,且当前桶存放的是一个链表,则依次遍历每个节点的 next 节点是否为空,为空则直接将当前元素放进链表,不为空则先判断两个 key 是否相等,相等则直接返回,不相等则继续判断 next 节点,直到 key 相等,或者 next 节点为空。

  4. 插入链表之后,如果当前链表长度大于 TREEIFY_THRESHOLD,默认是 8,则会将链表进行切换到红黑树存储。

处理完之后,最后还有一个判断就是判断是否覆盖旧元素,如果 e != null,则说明当前 key 值已经存在,则继续判断 onlyIfAbsent 变量,这个变量默认就是 false,表示覆盖旧值,所以接下来会进行覆盖操作,然后再把旧值返回。

在这里插入图片描述

扩容


HashMap 中存储的数据量大于阈值(负载因子 * 当前桶数量)之后,会触发扩容操作:

在这里插入图片描述

所以接下来让我们看看 resize 方法:

在这里插入图片描述

第一个红框就是判断当前容量是否已经达到了 MAXIMUM_CAPACITY,这个值前面提到了是 2 的 30 次幂,如果达到了这个值,就会将扩容阈值修改为 int 类型存储的最大值,也就是不会再出发扩容了。

最后

2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。

更多JVM面试整理:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-vWLUubLs-1713551504503)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值