这篇笔记是看完b站up主小刘讲源码对HashMap讲解之后做的记录和总结,用作学习复习。
如有错误欢迎各位积极指出!!!
大家可以去看up主的原视频,up主对于知识点的讲解非常到位,也别忘了给up主一键三连支持
视频链接:【HashMap源码分析,全网最细致版本,看完立拿offer!】 https://www.bilibili.com/video/BV1b84y1G7o5/?share_source=copy_web&vd_source=4e3e711bc94f815ee5aa99e0d66d8754
整理总结笔记不易。别忘了点赞关注收藏,这将是对我的最大支持
我们进入正题
目录
1.HashMap核心属性分析(threshold, loadFactory, size, modCount)
3.HashMap put 方法分析 => putVal方法分析
第一部分、基础入门
1.数组的优势/劣势
数组在内存上是连续的一片空间
优势:
从内存结构上看可知,他索引速度比较快,因为数组元素都有一个对应的下标index,当访问的时候可以通过数组下标index实现快速访问,也就是说查询速度较快。
劣势:
数组的大小是固定的,声明数组时数组的大小已经确定,想要改变大小只能重新声明一个数组然后将元素复制过去
2.链表的优势/劣势
链表在内存上是不连续的
优势:
从内存结构图上可以看出,链表的长度是不固定的,当插入元素删除元素时,可以在任意指定位置添加或删除元素,只需要把前一个元素指向该元素,该元素指向下一个即可,也就是说在对于增或者删操作的时候链表的速度是大于数组的。
劣势:
链表无法通过index下标直接获取到元素,先要获取元素只能挨个遍历,因此索引速度相较于数组较慢
3.有没有一种方式整合两种数据结构的优势?散列表
首先用了一个数组,然后数组中保存的数据是链表,散列表结构整合了数组和链表的各自优势。
4.散列表有什么特点?
散列表可以通过数组下标的index实现快速索引,又可以通过链表去动态扩容。
5.什么是哈希?
核心理论:Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。
这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。
Hash的特点:
- 从hash值不可以反向推导出原始的数据
- 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
- 哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值
- 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("暴躁","小刘");有如下几步
- 首先通过函数计算得到“暴躁”字符串的Hash值
- 经过Hash值扰动函数让Hash值变得更加散列均匀
- 构造出Node对象(属性有hash、key、value、next)
- 通过路由寻址公式找出node应该存放到数组的位置
路由寻址公式:(table.length - 1)& node.hash
5.什么是Hash碰撞?
不同元素的哈希值通过寻址运算之后有可能算出相同的位置index,这就Hash碰撞
6.什么是链化?
当发生Hash碰撞时,需要在碰撞位置上加入链表结构,通过Node元素的next属性指向下一个Node元素实现链化
7.jdk8为什么引入红黑树?
链化的缺点是当碰撞词元素越来越多,链表的长度越来越长,这样索引的效率会严重下降,因为链表索引是通过逐个遍历来实现的,索引jdk8中就引入了红黑树的方法来解决链表索引慢的问题
红黑树是一个平衡的二叉查找树,索引效率比较快
8.HashMap扩容原理?
在HashMap中储存的键值对达到一定数量之后会发生大量的哈希碰撞,这样会造成查找效率的严重下降,因此当存储的键值对数量超过了HashMap的负载因子,HashMap会进行扩容操作,以保持其性能。
HashMap的扩容原理如下:
- 初始化HashMap: 当创建一个HashMap时,会初始化一个初始容量(initial capacity)和负载因子(load factor)。初始容量指HashMap的初始大小,负载因子则是表示HashMap在容量达到多少时会进行扩容的比率。默认情况下,初始容量为16,负载因子为0.75。
- 插入键值对: 当往HashMap中插入键值对时,HashMap会根据键的哈希值确定存储位置,并将键值对存储在该位置。如果发生哈希冲突(即两个键的哈希值相同但键不同),则会采用链地址法解决冲突,即在同一个位置的链表中存储冲突的键值对。
- 检查是否需要扩容: 在插入键值对时,如果HashMap的当前大小(存储的键值对数量)超过了负载因子乘以容量(load factor * capacity),则会触发扩容操作。
- 扩容操作: 扩容操作会重新计算HashMap的容量,并创建一个新的更大的数组来存储键值对。通常情况下,新的容量会是原来容量的两倍。然后,HashMap会将原有的键值对重新分配到新的数组中,重新计算它们在新数组中的位置。这个过程需要遍历原有的数组,并重新计算哈希值,因此可能会比较耗时。
- 重新计算哈希值: 在扩容时,HashMap会根据键的哈希值和新的容量来重新计算键在新数组中的位置。由于容量改变,相同的哈希值可能会映射到新的位置,因此需要重新计算。
- 复制键值对: 扩容过程中,HashMap会将原有的键值对复制到新的数组中。这个过程涉及到数组元素的复制和重新分配,因此会消耗一定的时间和资源。
- 更新容量和负载因子: 扩容完成后,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自增并判断是否达到扩容阈值,达到触发扩容