浅谈HashMap底层基本原理

    相信大家对HashMap应该都不陌生吧,应该算是经常使用的集合类型了,最近因有看相关的文章,其实网上有很多的相关资料,我只是将自己理解的记录下来,在这里跟大家一起分享与学习

     首先我们来说下HashMap(基于jdk1.8),必然要知道HashMap 是什么(了解它的数据结构),有什么用(特点及优势)?

   首先需要了解下jdk1.7及之前hashMapd的底层是(数组+ 链表) jdk1.8及之后是(数组+链表+红黑树(提升查询效率))      

在Java中,最基本的数据结构其实也就2中(一个是数组,一个是链表),个人觉得大部分的数据结构都是由着2种结构来构造的。其实了解过HashMap的人都知道,hashMap其实底层是由:数据+链表 实现的,有的叫:链表散列;如下图

上面的Node就是数组中的元素,它是由一个next向下的引用,这就构成了(单向)链表。

   当我们在put 一个元素的时候,其实是根据key的hash值,去获取该元素在数组中的index(下标)的,然后将该元素放入到对应的位置中去,如果该位置有值了怎么办?为什么该位置会有值? 这些我们接下的会说到的。如果该位置已经有值了,那么就已链表的形式新加入的放表头(链头),最后加入的就放链尾。当我们在get的时候,首先会计算该key的hsahcode,在数组中找到对应index的元素,然后再通过key的equal的方法,找到需要的数据value,这个地方如果是链表的形式的话,那么在每个链表只有一个数据的时候,那效率还是挺高的,但是如果每个链表都有很多的值,那这个效率就很尴尬了(这也就是在jdk1.8之后做的一些优化,当链表的数据达到一定的数量的时候,就会将链表转为:红黑树o(logn)的效率)。

 那我们先来聊一聊hash算法(我们可以计算到下标(index),需要通过key的hash值来获取下标的),那是如何计算该位置的hash算法呢,首先这个下标既然是通过人工算出来的,那么就会有冲突问题,那么我们需要做的就是尽可能的讲这些数据分布的均匀些,尽量散落的均匀,这样就会减少每个链表的数据量,也就加快的hashmap的效率。

一般情况下,我们可以将每个位置的存储理解为一个二进制比如:15  - >  1111 存储的方式: 当位置为1的时候,说明该位置有值,有些人可能会问,为什么要减1 ,这就问道点上了,比如:一个长度的16的下标范围是(0~15),这样的下标范围,相信大家应该里理解为什么,需要减1 如果  不减的话 就是  0001 0000 ,这样是不是有些下标位置永远也不会有值的(如:0001,0010,0011,0101,1001,1011,1101),这样的话那么,空间的浪费就大了,而且也会导致链表的数据会多,导致查询效率降低.

^按位异或运算,只要位不同结果为1,不然结果为0;
>>> 无符号右移:右边补0

异或主要就是:能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢

为什么槽位必须是2^n

原因:1、为了让哈希后的结果更加均匀 。2、可以通过位运算e.hash & (newCap - 1)来计算,a % (2^n) 等价于 a & (2^n - 1)  ,位运算的运算效率高于算术运算,原因是算术运算还是会被转化为位运算)

这个原因我们继续用上面的例子来说明

假如槽位数不是16,而是17,则槽位计算公式变成:(17 - 1) & hash

计算了槽位(下标),就来聊聊如何扩容的

扩容(达到扩容的临界值,提高hashMap的查询效率,数据量一多hash碰撞就增加,因为如果数据量达到一定的量时,就会每个node节点就会产生很多值,就需要去变量,需要通过扩容保证效率问题)

1、什么时候扩容?

jdk1.7  (a、存放新值时,当前已有的值必须大于等于阀值(容量n【默认16】 * 负载因子【默认0.75】

             b、当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容,前11个节点都发生了hash碰撞,都存在某一节点,总共有15个node节点每个节点存值,最多:11+15个值,当第27个值进来时,则再发生扩容)

jdk1.8 (a、大于阀值,就扩容  注意:如果当前值不是新增,而是替换已有的值,那么也就不会发生扩容)

2、如何扩容?

 步骤:1、创建一个新的Entry空数组,长度是原数组的2倍。

           2、ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变 

JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置, 在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上

推荐位大神的博客:https://blog.csdn.net/pange1991/article/details/82347284  个人觉得例子写的挺好的

猜你喜欢

转载自blog.csdn.net/u010200793/article/details/105161493