HashMap源码分析,面试重点,全网最细致版本(课程笔记总结)

这篇笔记是看完b站up主小刘讲源码对HashMap讲解之后做的记录和总结,用作学习复习。

如有错误欢迎各位积极指出!!!

大家可以去看up主的原视频,up主对于知识点的讲解非常到位,也别忘了给up主一键三连支持

视频链接:【HashMap源码分析,全网最细致版本,看完立拿offer!】 https://www.bilibili.com/video/BV1b84y1G7o5/?share_source=copy_web&vd_source=4e3e711bc94f815ee5aa99e0d66d8754

整理总结笔记不易。别忘了点赞关注收藏,这将是对我的最大支持


我们进入正题

目录

第一部分、基础入门

1.数组的优势/劣势

2.链表的优势/劣势

3.有没有一种方式整合两种数据结构的优势?散列表

4.散列表有什么特点?

5.什么是哈希?

第二部分,HashMap原理讲解

1.HashMap的继承体系是什么样的?

2.Node数据结构分析?

3.底层存储结构介绍?

4.put数据原理分析?

5.什么是Hash碰撞?

6.什么是链化?

7.jdk8为什么引入红黑树?

8.HashMap扩容原理?

第三部分,手撕源码

1.HashMap核心属性分析(threshold, loadFactory, size, modCount)

2.构造方法分析

3.HashMap put 方法分析 => putVal方法分析

4.HashMap resize 扩容方法分析(核心)

5.HashMap get 方法分析

6.HashMap remove 方法分析

7.HashMap replace 方法分析


第一部分、基础入门


1.数组的优势/劣势

数组在内存上是连续的一片空间

优势:

从内存结构上看可知,他索引速度比较快,因为数组元素都有一个对应的下标index,当访问的时候可以通过数组下标index实现快速访问,也就是说查询速度较快。

劣势:

数组的大小是固定的,声明数组时数组的大小已经确定,想要改变大小只能重新声明一个数组然后将元素复制过去


2.链表的优势/劣势

链表在内存上是不连续的

优势:

从内存结构图上可以看出,链表的长度是不固定的,当插入元素删除元素时,可以在任意指定位置添加或删除元素,只需要把前一个元素指向该元素,该元素指向下一个即可,也就是说在对于增或者删操作的时候链表的速度是大于数组的。

劣势:

链表无法通过index下标直接获取到元素,先要获取元素只能挨个遍历,因此索引速度相较于数组较慢


3.有没有一种方式整合两种数据结构的优势?散列表

首先用了一个数组,然后数组中保存的数据是链表,散列表结构整合了数组和链表的各自优势。


4.散列表有什么特点?

散列表可以通过数组下标的index实现快速索引,又可以通过链表去动态扩容。


5.什么是哈希?

核心理论:Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。
这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。


Hash的特点:

  1. 从hash值不可以反向推导出原始的数据
  2. 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
  3. 哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
  4. hash算法的冲突概率要小

由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间,也就是会发生哈希值碰撞。
根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。


抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果,这一现象就是我们所说的“抽屉原理”。


第二部分,HashMap原理讲解

1.HashMap的继承体系是什么样的?

HashMap继承了AbstractMap抽象类,AbstractMap抽象类实现了java.util.Map接口

2.Node数据结构分析?

Node是HashMap类中的一个静态内部类,实现了Map.Entry<K,V>接口

在Map.Entry<K,V>接口中有getKey()、getValue()、setValue(V)等方法

Node结构当中hash、key、value、next

hash:key的哈希值经过一个扰动之后存储在这里(扰动函数,目的是使得分布更均匀)

                           k的hash值  → 扰动函数 → hash值

key、value:Map中的key和value

next:当发生哈希碰撞的时候使用这个next将碰撞区域以链表形式连起来


3.底层存储结构介绍?

最外层是一个Node数组,默认初始化长度是十六

当发生哈希冲突时,冲突的位置会形成一个链表,当链表长度超过8之后链表结构将升级成为红黑树

HashMap的底层结构就是 数组+链表+红黑树来实现的


4.put数据原理分析?

当要put一个元素时例如map.put("暴躁","小刘");有如下几步

  1. 首先通过函数计算得到“暴躁”字符串的Hash值
  2. 经过Hash值扰动函数让Hash值变得更加散列均匀
  3. 构造出Node对象(属性有hash、key、value、next)
  4. 通过路由寻址公式找出node应该存放到数组的位置

路由寻址公式:(table.length - 1)& node.hash


5.什么是Hash碰撞?

不同元素的哈希值通过寻址运算之后有可能算出相同的位置index,这就Hash碰撞


6.什么是链化?

当发生Hash碰撞时,需要在碰撞位置上加入链表结构,通过Node元素的next属性指向下一个Node元素实现链化


7.jdk8为什么引入红黑树?

链化的缺点是当碰撞词元素越来越多,链表的长度越来越长,这样索引的效率会严重下降,因为链表索引是通过逐个遍历来实现的,索引jdk8中就引入了红黑树的方法来解决链表索引慢的问题

红黑树是一个平衡的二叉查找树,索引效率比较快


8.HashMap扩容原理?

 在HashMap中储存的键值对达到一定数量之后会发生大量的哈希碰撞,这样会造成查找效率的严重下降,因此当存储的键值对数量超过了HashMap的负载因子,HashMap会进行扩容操作,以保持其性能。

HashMap的扩容原理如下:

  1. 初始化HashMap: 当创建一个HashMap时,会初始化一个初始容量(initial capacity)和负载因子(load factor)。初始容量指HashMap的初始大小,负载因子则是表示HashMap在容量达到多少时会进行扩容的比率。默认情况下,初始容量为16,负载因子为0.75。
  2. 插入键值对: 当往HashMap中插入键值对时,HashMap会根据键的哈希值确定存储位置,并将键值对存储在该位置。如果发生哈希冲突(即两个键的哈希值相同但键不同),则会采用链地址法解决冲突,即在同一个位置的链表中存储冲突的键值对。
  3. 检查是否需要扩容: 在插入键值对时,如果HashMap的当前大小(存储的键值对数量)超过了负载因子乘以容量(load factor * capacity),则会触发扩容操作。
  4. 扩容操作: 扩容操作会重新计算HashMap的容量,并创建一个新的更大的数组来存储键值对。通常情况下,新的容量会是原来容量的两倍。然后,HashMap会将原有的键值对重新分配到新的数组中,重新计算它们在新数组中的位置。这个过程需要遍历原有的数组,并重新计算哈希值,因此可能会比较耗时。
  5. 重新计算哈希值: 在扩容时,HashMap会根据键的哈希值和新的容量来重新计算键在新数组中的位置。由于容量改变,相同的哈希值可能会映射到新的位置,因此需要重新计算。
  6. 复制键值对: 扩容过程中,HashMap会将原有的键值对复制到新的数组中。这个过程涉及到数组元素的复制和重新分配,因此会消耗一定的时间和资源。
  7. 更新容量和负载因子: 扩容完成后,HashMap会更新自身的容量和负载因子,以便后续的插入操作。

第三部分,手撕源码

1.HashMap核心属性分析(threshold, loadFactory, size, modCount)

  常量分析:

 数组默认大小1左移4位是16

数组最大长度

负载因子,用来判断是否扩容

链表树化的阈值,链表长度超过8才会树化

树降级成为链表的阈值

树化的另一个阈值,当哈希表中键值对数量超过64时才会允许树化,

核心属性分析:

哈希表

当前哈希表中元素个数

当前哈希表结构修改次数

扩容阈值,当哈希表中的元素超过阈值时触发扩容

负载因子,默认0.75  

扩容阈值 threshold = 负载因子乘以容量(load factor * capacity)

2.构造方法分析

第一个参数是initialCapacity容量大小,第二个参数是loadFactor

首先进行了数值校验,容量capacity不能小于0和超过最大值

然后负载因子loadFactor值不能小于0也不能非数

然后将入参loadFactor传递给自身属性loadFactor

因为hashmap的长度和扩容阈值数threshold只能是2的整数次方数,因此capacity容量需要通过tablesizeFor方法返回一个2的整数次方数

tablesizeFor方法:


3.HashMap put 方法分析 => putVal方法分析

HashMap的put方法内部调用的是一个putVal方法,putVal入参又有一个hash方法

上面说到根据node元素key的hash值经过扰动函数之后得到的hash值与table长度进行运算之后可以得出位置,这里的hash()函数就是那个扰动函数

作用:让key的hash值的高16位也参与路由运算

当哈希值为null,返回0

不为零则进行与自身右移十六位异或运算并返回(高十六位与低十六位异或)

这里运算的原因是因为hashtable数组的默认长度是16位扩容几次也只有32、64长度很小,寻找位置时的路由算法是(table.length - 1)& node.hash,table.length - 1的值很小,所以只有进行扰动运算才能让hash的高十六位也参与到寻址运算当中,让结果值更加均匀。

 putVal方法:

参数:

hash、key、value 就是node的属性

onlyIfAbsent 如果散列表中已经存在key,如果key已经存在就不插入

但是一般都传入false,有则替换,无则插入

tab:表示当前hashMap的散列表

p:表示当前散列表的元素

n:表示散列表数组的长度

i:表示路由寻址 结果

将table赋值给tab并判断是否为null,如果为null或者长度等于0,需要创建一个散列表

这里是延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象汇总的最小号内存的散列表

通过路由算法算出位置,然后判断这个桶位置是否为null,如果为null直接用键值对new一个Node放入这个位置

如果这个位置不为null,也就是发生了碰撞

e:Node临时元素

k:临时的一个key

p:当前桶位置的元素

如果当前位置的元素的hash与插入元素的hash与当前位置的元素的key与插入元素的key或者插入的key不为空情况下插入的key和当前位置的key相等  

那么就将插入元素p赋值给e

这里判断如果这个桶位置已经树化

最后只剩下一种链表的情况,而且链表的头元素的key与要插入的key不一致

循环这个链表,然后判断该节点元素的next是否为null,

第一种情况:

p.next为null,也就是循环到了链表最后一个元素,说明没有找到符合的key

如果是最后一个则在链表末尾插入元素

然后判断长度是否达到树化阈值,达到的话则树化

第二种情况:

条件成立说明找到了相同key的Node元素,进行替换即可

如果e不等于null,条件成立说明,找到一个key相同的元素,替换即可,完成替换return

散列结构修改次数加一,替换不加,size自增并判断是否达到扩容阈值,达到触发扩容

博主睡觉去了,剩下的后续更新


4.HashMap resize 扩容方法分析(核心)


5.HashMap get 方法分析


6.HashMap remove 方法分析


7.HashMap replace 方法分析

  • 27
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值