每日面试题打卡(容器篇)——Day10

博主个人博客网站:文客
这个系列会长期更新!
如果你想每天和我打卡面试题、交流技术,可以关注一下我的个人博客网站:文客,我会每天在这里更新技术文章和面试题,也会及时收到大家的评论与留言,欢迎各位大佬来交流!

说一下HashMap的实现原理

HashMap主要用来存储键值对,它基于哈希表的Map接口实现,是最常用的Java集合之一,是非线程安全的。

HashMap可以存储null的key和null的value,不过为null的key只能有一个,而null的value可以有多个。

从底层数据结构来说,在JDK1.8之前,HashMap由“数组 + 链表”实现,数组是HashMap的主体,链表则是为了解决哈希冲突而存在的(拉链法解决哈希冲突)。在JDK1.8以后,HashMap在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为8),这时会判断数组的长度,如果数组长度小于64,那么会选择对数组扩容,如果大于等于64,那么会将链表转换为红黑树,提高搜索效率。

HashMap是基于Hash算法实现的:

  1. 当我们put一个元素时,利用key的hashCode重新hash计算出当前元素在集合中的下标
  2. 如果出现hash值相同的key,此时有两种情况:如果两个key相同,那么覆盖原始值;如果两个key不同,则将当前的值放入链表中
  3. 获取时,直接找到hash值对应的下标,再进一步判断key时候相等,从而找到对应的值

HashMap的默认的初始化大小是16,之后每次扩容都会变为原来的两倍。并且,HashMap总是使用2的幂作为哈希表的大小。

HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

在JDK1.8之前,HashMap的底层数据结构时“数组 + 链表”,也叫做链表散列。

HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的n指的是数组长度),如果当前位置存放元素的话,那么就判断与要存入的数据的key是否相同,如果相同,直接覆盖,不相同就用拉链法解决冲突。

所谓扰动函数就是指的HashMap的hash方法,使用hash方法就是为了防止一些比较差的hashCode()方法,换句话说使用扰动函数之后可以减少碰撞。

所谓拉链法就是将链表与数组结合,也就是一个链表数组,数组中的每一个格就是链表的头,如果遇到哈希冲突,将冲突的值直接放入链表即可。

再来说一下JDK1.8之前HashMap的put方法,HashMap对于添加元素只提供了put方法,putVal方法是内部提供给put方法调用的。在put时,如果当前定位到的方法没有元素,则直接插入,如果当前定位到的位置有元素,遍历以这个元素为头节点的链表,依次和插入的key比较,如果key相同则直接覆盖,如果不同就采用头插法插入元素。

在JDK1.8以后,相较于之前的版本,JDK1.8在解决哈希冲突上有了较大的变化。

当链表长度大于阈值(默认为8)时,首先会根据HashMap数组的情况来决定是否转变为红黑树,如果数组的长度小于64,那么先会对数组进行扩容,如果数组长度大于等于64,此时将链表转换为红黑树,以减少搜索时间。

同时JDK1.8也对扰动次数进行了优化,减少了扰动次数,效率比JDK1.8之前更高。

JDK1.8也对HashMap的put 方法进行了改动,如果定位到的位置没有元素,则直接插入,如果定位到的位置有元素,遍历以该位置为头节点的链表,如果有相同的key则直接覆盖。如果没有相同的key,就判断节点是否为树节点,如果是那就调用putTreeVal方法将元素加入,如果不是就尾插法插入(JDK1.7是头插)。

HashMap的加载因子loadFactory

loadFactory加载因子用于控制数组存放数据的疏密程度,loadFactory越趋近于1,那么数组中存放的数据也就越多,也就越密,也就是会让链表的长度增加。loadFactory越小,也就是趋近于0,数组中存放的元素也就越少,越稀疏。

loadFactory太大会导致查找元素效率低,太小导致数组利用率低,存放的数据很分散。loadFactory的默认值是0.75f,这是官方给出的比较好的临界值。

HashMap的默认容量是16,负载因子为0.75f,当数据量达到16 * 0.75 = 12 时就需要将当前16的容量进行扩容,而扩容过程涉及到rehash、复制数据等操作,非常消耗性能。

HashMap的装载因子threshold

装载因子 = 容量 * 加载因子,它是一个衡量数组是否需要扩容的一个标准,如果size大于了装载因子,那么就要考虑对数组进行扩容了。

HashMap的put方法的具体流程?

put操作首先会判断键值对数组table[i]是否为空或者为null,否则执行resize()方法进行扩容。然后根据key的hashCode计算hash值计算索引i,这里会调用hash函数,hash函数大概作用是:高16bit不变,低16bit和高16bit做一次异或操作,目的是为了减少碰撞,如果table[i]为空,直接新建节点添加,如果table[i]不为空,则会依次判断是否有相同的key,如果相同则直接覆盖,如果不同则会判断当前节点是否为树节点,如果是树节点,调用putTreeVal方法插入键值对,如果不是树节点,遍历链表,判断链表长度是否大于8,如果大于8,将链表转化为红黑树再进行插入操作,如果小于8,直接尾插到链表。插入成功后,判断实际存在的键值对数量size是否超过了装载因子,如果超过,进行resize扩容。

HashMap的扩容操作是怎么实现的?

  1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
  2. 每次扩展的时候,都是扩展2倍;
  3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于负载因子(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

博客原文地址

每日面试题打卡(容器篇)——Day10
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

九天漩女

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值