金三银四助力面试-手把手轻松读懂HashMap源码(2),字节跳动数据库面试题

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

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

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

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

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

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

正文

当我们手动指定容量初始化 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 类型存储的最大值,也就是不会再出发扩容了。

第二个红框就是扩容,扩容的大小就是将旧容量左移 1 位,也就是扩大为原来的 2 倍。当然,扩大之后的容量如果不满足第二个红框中的条件,则会在第三个红框中被设置。

扩容之后原有数据怎么处理

扩容很简单,关键是原有的数据应该如何处理呢?不看代码,我们可以大致梳理出迁移数据的场景,没有数据的场景不做考虑:

  1. 当前桶位置只有自己,也就是下面没有其他元素。

  2. 当前桶位置下面有元素,且是链表结构。

  3. 当前桶位置下面有元素,且是红黑树。

接下来让我们看看源码中的 resize 方法中的数据迁移部分:

在这里插入图片描述

红框部分比较好理解,首先就是看当前桶内元素是否是孤家寡人,如果是,直接重新计算下标然后赋值到新数据即可,如果是红黑树,则打散了重组,这部分暂且略过,最后一个 else 部分就是处理链表部分,接下来就让我们重点看一下链表的处理。

链表数据处理

链表的处理有一个核心思想:链表中元素的下标要么保持不变,要么在原先的基础上在加上一个 oldCap 大小

链表的数据处理完整部分源码如下图所示:

在这里插入图片描述

关键条件就是 e.hash & oldCap,为什么这个结果等于 0 就表示元素的位置没有发生改变呢?

在解释这个问题之前,需要先回忆一下 tableSizeFor 方法,这个方法会将 n-1 调整为类似 00001111 的数据结构,举个例子:比如初始化容量为 16,长度 n-1 就是 01111,而 n 就是 10000,所以如果 e.hash & oldCap ==0 就说明 hash 值的第 5 位是 010000 扩容之后得到的就是 100000,对应的 n-1 就是 011111,和原先旧的 n-1 的差异之处就是第 5 位(第 6 位是 0 不影响计算结果),所以当 e.hash & oldCap==0 就说明第 5 位对结果也没有影响,那么就说明位置不会变,而如果 e.hash & oldCap !=0,就说明第 5 位数影响到了结果,而第 5 位如果计算结果为 1,则得到下标的位置恰好多出了一个 oldCap 的大小,即 16。其他位置扩容也是同样的道理,所以只要 e.hash & oldCap==0,说明下标位置不变,而如果不等于 0,则下标位置应该再加上一个 oldCap

最后的循环完节点之后,处理源码如下所示:

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

三面蚂蚁金服成功拿到offer后,他说他累了

三面蚂蚁金服成功拿到offer后,他说他累了

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

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

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

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值