您当前的位置: 首页 > 

星夜孤帆

暂无认证

  • 2浏览

    0关注

    626博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

JDK7HashMap源码

星夜孤帆 发布时间:2021-04-08 19:31:44 ,浏览量:2

一、数组

二、ArrayList

三、HashMap 3.1 HashMap基本实现思路 3.1.1 根据key得到数组下标

HashMap是采用数组+链表的方式实现

那HashMap为什么不采用和ArrayList同样的方式,保存在数组中,而还要加上链表

因为,HashMap保存的是(key,value)的形式,get的时候是根据key来取值,而不是像ArrayList通过下标可以直接获取。

但是,思路都是一样的,我总得有一个下标,来存储map的(key,value)吧,那这个下标究竟是怎么来的呢?

其实就是通过这个key来确定的,它通过key.hashCode方法来获取到一个值

通过上图分析,假如我现在有一个key=c,有一个value=3,然后,通过c.hashCode得到了一个值,然后,对他进行c.hashCode % array.length的操作,得到了一个下标,就可以将其存到数组中了。

3.1.2 链表解决冲突

不过值得注意的是,我这个key是随机的,通过key.hashCode得到值可能不一样,但是经过取余操作有可能就一样了,那这样会造成一个什么后果?

有可能我这个数组同一个位置,会得到多个(key,value),也可能有一些位置,会一个(key,value)也没有。

那当一个位置出现多个(key,value)的时候,就需要我们用链表来解决了。

3.1.3 get元素

我们把对应的元素put进去以后,怎么get到呢?

跟put时类似,我们肯定还是需要先确定下标的位置

get(key) {
    int hashCode = key.hashCode();
    int index = hashcode % array.length();
    Node header = array[index];
}

那究竟怎么下移呢

//数据存在链表里,不停的更新头节点的指针
put(key, value) {
    int hashCode = key.hashCode();
    int index = hashCode % array.length();
    //array[index] = new Entry(key, value, null);
    array[index] = new Entry(key, value, array[index]);//array[index],就是之前的链表,然后,        
    将引用又赋值给array[index]
}

3.2 Jdk7 HashMap源码 3.2.1 HashMap中的变量

3.2.2 HashMap中构造方法

3.2.3 HashMap put方法

1.put为null

2.highestOneBit

为什么要右移16位?

右移的次数1+2+4+8+16=31,也就是一共移动了31位,而int类型正整数的范围就是2的31次方。int类型是四个字节,32个bit位

3. roundUpToPowerOf

4.inflatTable初始化数组

初始化数组为什么一定要2的幂次方数呢?

为了保证计算下标时,hash % length == hash & (length -1)

那它为什么不用取余呢,因为位运算相比于取余会快很多

5.hash(key)

根据步骤6的解释,我们再来回到计算hash值的方法

为什么计算hash值,这边不是单纯的计算key.hashCode,而是有很多右移操作呢?

这样的话,会引起什么问题,就是不论key计算的hash值是多少,只要,它的低位是一样的,最后计算得出的索引位置,它就是一样的。这样势必会更容易引起hash冲突,得到的链表也随之会非常非常的长。

为了解决这个问题,我们就应该想办法让这个hash值的高位也参与到计算数组下标的值里面来,

扰动函数

6.indexFor(hash,table.lenth)

计算数组的下标

1) 保证不会越界

比如:传入16,我这个值的范围只能是0-15

2) 保证我这个0-15这些个数字,出现的频率要一样,尽量的平均

所以,取余操作是正好满足的

这个值,低四位全是1,那经过&操作,算出来的值,其实就是hashcode的低四位。

而低四位的取值范围就是0000-1111正好是在0-15范围内

对于,平均,因为hashcode的值是随机的,所以得到的索引值也是随机的,所以,这两个条件都达到了。

·这就解释了,之前数组的容量必须要保证是2的幂次方数,只有是2的幂次方才能保证length -1 的二级制高位全是0,低位全是1

7.map.put(key,value)解决key相同问题

对于链表,有可能会存入key相同的值,这边就会先遍历链表,如果原来链表上的key与当前put的key相同,就将原来的value返回,并用新的value将其覆盖。

这里有个问题,为什么都是要遍历,干嘛不采用尾插法把元素添加到尾部,还要用头插法+整体移动呢?

综上,如果put的key相等,就不用遍历整个链表了,直接就返回结果了。

如果采用尾插法的话,肯定每一次都要遍历到尾部的,所以,采用头插法还是要快的。

8.addEntry

9.resize扩容

如果,hashMap里面存的数量大于等于table.length * 0.75 ,并且要插入的元素索引位置为null,才会触发扩容操作

值得注意的是,这个判空只在jdk7有,jdk8已经没有了。

对于一个map来说,有数组和链表组成,而就扩容而言,仅仅指数组的扩容,链表是没有这种说法的,你想往链表里面存多少就存多少。

假设,现在有两个线程,调用同一个hashmap的put方法,两个线程同时走到resize方法,都会生成一个翻倍的数组,另外,又都进到了这个transfer方法里面了,都会进行这个while的双重循环,也就是都会遍历老数组里面的每一个元素,我们刚才看到的e或是next

都是这个方法里面的局部变量,所以,每一个线程里面这个e或是next肯定都是有的

jdk7里面扩容可能会导致循环链表出现。

怎么尽可能避免循环链表出现呢?

只有扩容的时候才会导致循环链表出现,那么我们如果我们预先知道要存多少个元素,创建hashmap的时候指定好它的容量,尽可能避免hashmap扩容,这样就可以有效的防止死循环的出现。

扩容的目的是什么呢?

其实就是为了让链表变短一些,因为扩容后,对应的数组下标可能会有两个,一个是原数组索引位置,另一个就是原数组索引位置+原数组的长度。这样的话,我们get元素的时候,就会加快。

10.inithashSeedAsNeeded

11.modCount

fail-fast快速失败机制

这里我们只是发现了这个modCount会出现异常,以及它的解决办法,但是,modCount它这个意义何在呢,它为什么要这么做呢?

这其实是一种容错机制:fail-fast,快速失败机制

当hashMap发现现在有问题了,那么它就直接抛一个异常,就不让你的代码继续执行下去了,这就是快速失败。

它为什么会认为我们刚才那个场景是有问题的呢?

假设,我们现在有两个线程,其中一个线程,在遍历,另一个线程在进行put或者在remove操作,这个时候就会出现并发的问题。

两个线程,一个在遍历,一个在修改同一个map,肯定会出现并发的问题。

hashmap它知道会出现这个问题的,因为它本身就不是并发安全的,当它发现确实遇到这个问题了,它就会抛这么一个异常。所以说,这是一种快速失败的机制。

3.2.4 HashMap get方法

1.get方法

2.getForNullkey

3.getEntry(key)

视频教程

关注
打赏
1636984416
查看更多评论
立即登录/注册

微信扫码登录

0.0376s