Hashmap 的源码解析以及put流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011066470/article/details/86699062

一.Hashmap的定义

Hashmap是一种常用的数据结构,底层是基于数组和链表实现的。

二.Hashmap的详情结构

2.1 数据结构

HashMap提供了三个构造函数:

      HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

      HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

      HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

      在这里提到了两个参数:初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。

加载因子:0.75,在容量达到0.75时,进行扩容。

2.2 整体的数据结构

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好

 

2.3 put操作的代码分析

2.3.1 entry类

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

概图浏览:

一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。

2.3.2 PUT操作

两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?为了解决这个问题,HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。

上图中代码的意思:key的hashcode值一样且key串内容也一样

如《liu,23》,《liu,34》;存储逻辑为:


createEntry方法中:表达的逻辑为:如图

Put方法的逻辑过程:

1.首先判断key是否为null,若为null,则直接调用putForNullKey方法。

2.若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,

static int indexFor(int h, int length) {

        return h & (length-1);

    }

计算存储位置的方法,hashcode和长度-1做与运算

2.1.若该位置没有元素,则直接插入。

2.2.如果table数组在该位置处有元素,则依次迭代遍历元素的key的内容值。

2.2.1如果两个hash值相等且key的内容值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value

2.2.2如果两个hash值相等但key的内容值不等 ,则将该节点插入该链表的链头。(最先保存的元素放在链尾)

2.4 hashmap的get

相对于HashMap的存而言,取就显得比较简单了。通过key的hash值找到在table数组中的索引处的Entry,然后通过key的equals方法找到存储的对象,然后返回该key对应的value即可。

https://www.cnblogs.com/dassmeta/p/5338955.html

https://www.cnblogs.com/chengxiao/p/6059914.html

https://blog.csdn.net/xiaokang123456kao/article/details/77503784

https://blog.csdn.net/a_long_/article/details/51594159

2.5 hashmap的扩容

通过以上代码能够得知,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作

通过源码可以发现,hashMap的数组长度一定保持2的次幂,这样做有什么好处呢?

2^n转换成二进制就是1+n个0,减1之后就是0+n个1,如16 -> 10000,15 -> 01111

那么根据&位运算的规则,都为1(真)时,才为1

比如length为16,则length-1为15,对应的二进制为01111

与hashcode:1001,1000运算还是hashcode本身,尽量分布均匀,减少会造成空间的浪费

比如length为15,则length-1为14,对应的二进制为1110

与1001,1000最后都为1000,发生碰撞,存储到同一个位置的链表中

可以减少Hash碰撞

如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),

数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀

https://blog.csdn.net/wanghuan220323/article/details/78242449

都看到这里了,就顺手点击左上角的【关注】按钮,点击右上角的小手,给个评论,关注一下,再走呗!☺

猜你喜欢

转载自blog.csdn.net/u011066470/article/details/86699062
今日推荐