大厂必问的HashMap底层原理之一

HashMap是以key-value键值对存储的,jdk1.8以后,底层的数据结构是数组+链表+红黑树,数组是HashMap主体,链表是为了解决hash冲突解决的(hash冲突是指两个对象调用hashCode()计算出的哈希值一致导致计算的数组索引值相同),也称“拉链法”解决哈希冲突。当链表长度大于阈值(默认为8)时并且数组的长度大于64时,链表将会转换为红黑树,加入红黑树是为了高效的查询。
注意:如果链表长度大于阈值(默认为8)时并且数组的长度小于64时,链表不会转换为红黑树,而是将数组进行扩容。这样做的目的考虑到数组的长度较小,尽量避开红黑树,这种情况下,转为红黑树,反而会降低效率问题,因为红黑树需要左旋,右旋,变色操作来保持平衡,所以当数组长度小于64,搜索时间会相对快些,考虑到性能和减少搜索的时间,只有当链表长度大于阈值(默认为8)时并且数组的长度大于64时,链表将会转换为红黑树。

存储元素的特点:
1)无序
2)key和value都可以为null
一:底层数据结构
数组+链表+红黑树
假设我们执行一下代码:
在这里插入图片描述
1,jdk1.8以后,但我们创建hashMap集合对象以后,不会在构造方法中创建数组,而是第一次调用put()方法时创建一个长度为16的Node<K,V>[ ] table的数组。
2,假设我们第一次向hashMap添加元素(“重地”,100)时,会根据String类重写的hashCode()计算出“重地”的相对应的hash值,根据此hash值并结合数组的长度采用某种算法计算出存储数组中的索引值,如果此数组索引上没有值,则直接存到数组中。
3,假设我们第二次向hashMap添加元素(“通话”,100)时,我们算出“通话”的hash值与上一次添加的“重地”的hash值一致,此时数组上索引空间不为null,此时会发生hash冲突,再调用String的equal()。比较两个键的内容是否一致,此时明显键的内容不一致,我们再去链表中挨个比较,如果都不相同,则在空间划出一个节点存储该(“通话”,100)键值对。
4,假设我们第三次向hashMap添加元素(“通话”,200)时,由于hash值和equals()比较的内容都一致,我们将新的value覆盖原来的value。
5,假设我们第四次向hashMap添加元素(“耕耘”,400)时,假设根据此hash值并结合数组的长度采用某种算法计算出存储数组中的索引值与“重地”的索引值相同,但我们会比较hash是否一致,如果hash值不一致,则在空间划出一个节点存储该“耕耘”,400)键值对。
注意:我们上面提到某种算法,到底是什么算法算出的索引空间?结合源码我们看到:
主要通过无符号右移>>>,按位异或^,按位与&得到的
在这里插入图片描述通过源码我们看出:当我们调用put()以后,根据上图的蓝色小箭头,我们看到先通过hash()里的无符号右移>>>和按位异或^算出key对应的hash值,底层又调用了putVal(),将数组长度减1和上一步得到的hash值进行按位与&运算,最后得出该键值对具体存在数组的那个索引空间下。
我们想到计算最为直观的想法是:hash%n,即通过取余的方式把当前的key、value键值对散列到各个桶中;那么这里为什么不用取余(%)的方式呢?原因是CPU对位运算支持较好,即位运算速度很快。 另外,当n是2的整数次幂时:hash&(n-1)与hash%n是等价的,但是两者效率来讲是不同的,位运算的效率远高于%运算。基于这一点,HashMap中使用的是hash&(n-1)。
二,具体的put()操作过程如下:
1,首先会判断table是否初始化,如果没有初始化,调用resize()方法,创建一个长度为16的数组
在这里插入图片描述在这里插入图片描述2,如果table已经初始化,根据hash值和数组的长度计算所在数组的索引空间是否为空,如果为空,则直接放入table数组中。如果索引空间不为空,说明已经有数据存在,
在这里插入图片描述3,判断一下两个对象的hash值和key的值是否相等,如果都相同,则新的值覆盖旧的值

在这里插入图片描述4,如果不同,则进行如下的判断:
先判断是不是红黑树结构,如果是直接放入红黑树中,如果不是,循环链表判断是否有相同的key,有的话就做修改操作,如果没有,直到循环链表最后一个节点没有数据,再插入,并且判断是否需要转为红黑树。
在这里插入图片描述5,在每次添加的时候都要判断当前数组的长度是否大于临界值,如果大于,则进行扩容
size:当前数组的长度
threshold:默认状态下为12,初始数组容量16乘以加载因子0.75,
在这里插入图片描述
6,扩容机制:增加为原来的一倍
在这里插入图片描述三,属性
1,为什么数组容量必须是2的n次幂?
创建HashMap时未指定初始容量情况下的默认容量16(必须是2的n次幂)
在这里插入图片描述为什么容量必须是2的n次幂?我们发现通过数组长度减一在与hash值做按位与运算,能够均匀地减少hash碰撞,2的n次方在二进制中,实际就是1后面有n个零,2的n次方减1在二进制中,实际就是n个1,按位与运算:相同二进制位上,都是1的时候结果为1,否则为0。如果不是2的n次幂,计算出的但索引特别容易相同,产生hash碰撞较大,导致其他索引上的值很大程度上没有发生存储数据,导致链表或者红黑树过长。
三:如果我们创建集合时,自己输入的值不是2的n次幂会怎么办?
假如我们输入10,则会将值转换为16,即比这个数大的最小的2的n次幂
通过源码我们能看出:通过无符号右移和按位或运算,最后得出2的n次幂
在这里插入图片描述下面我们分析该算法:为什么cap要做减一操作呢,为了避免如果cap是2的n次幂,如果没有执行减一操作,执行完运算以后,返回的值将会是cap的2倍
通过无符号右移和按位或运算,最后都会得出n个1,最后加1操作,都会变成2的n次幂
按位或运算:相同二进制位上,都是0结果为0,否则为1。
2,默认的加载因子0.75
在这里插入图片描述当数组中的容量大于(初始容量16默认加载因子0.75)12时,则会进行扩容
3,当链表长度大于8,并且数组长度大于64时候,链表会转为红黑树
在这里插入图片描述
面试题:为什么在8的时候会转为红黑树?
因为树的节点大小大约是普通节点的两倍,只有当桶中的节点足够多的时候才会转换成红黑树,说白了就是时间和空间的复杂度的权衡,而且根据统计学上分析,在链表长度大于8的情况小之又小。
4,当红黑树的节点数量<=6时,红黑树转为链表
在这里插入图片描述5,链表转为红黑树时对应数组的最小长度
在这里插入图片描述6,存储元素的数组
在这里插入图片描述
7,存储元素的个数,并不是数组table的长度
在这里插入图片描述
8,边界值,当数组的实际大小大于(容量
加载因子)时,进行扩容
在这里插入图片描述
10,加载因子是衡量桶的利用率,当它较小时,桶的利用率就小,浪费空间就大。put()元素时发生冲突的概率就小,get(),put()的操作代价就很低;当它较大时,浪费的空间小,但get()和put()的代价就大

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值