JDK7HashMap源码

一、数组

二、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)

视频教程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
HashMap 是一种哈希表数据结构,它实现了 Map 接口,可以存储键值对。下面是 JDK 8 中 HashMap源码详解。 1. 基本概念 哈希表是一种基于散列原理的数据结构,它通过将关键字映射到表中一个位置来访问记录,以加快查找的速度。在哈希表中,关键字被映射到一个特定的位置,这个位置就称为哈希地址或散列地址。哈希表的基本操作包括插入、删除和查找。 2. 类结构 HashMap 类结构如下: ``` public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... } ``` HashMap 继承了 AbstractMap 类,并实现了 Map 接口,同时还实现了 Cloneable 和 Serializable 接口,表示该类可以被克隆和序列化。 3. 数据结构 JDK 8 中的 HashMap 采用数组 + 链表(或红黑树)的结构来实现哈希表。具体来说,它使用了一个 Entry 数组来存储键值对,每个 Entry 对象包含一个 key 和一个 value,以及一个指向下一个 Entry 对象的指针。当多个 Entry 对象的哈希地址相同时,它们会被放入同一个链表中,这样就可以通过链表来解决哈希冲突的问题。在 JDK 8 中,当链表长度超过阈值(默认为 8)时,链表会被转化为红黑树,以提高查找的效率。 4. 哈希函数 HashMap 的哈希函数是通过对 key 的 hashCode() 方法返回值进行计算得到的。具体来说,它使用了一个称为扰动函数的算法来增加哈希值的随机性,以充分利用数组的空间。在 JDK 8 中,HashMap 使用了以下扰动函数: ``` static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 其中,^ 表示按位异或,>>> 表示无符号右移。这个函数的作用是将 key 的哈希值进行扰动,以减少哈希冲突的概率。 5. 插入操作 HashMap 的插入操作是通过 put() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置是空的,就直接将 Entry 对象插入到该位置;否则,就在该位置对应的链表(或红黑树)中查找是否已经存在具有相同 key 的 Entry 对象,如果存在,则更新其 value 值,否则将新的 Entry 对象插入到链表(或红黑树)的末尾。 6. 查找操作 HashMap 的查找操作是通过 get() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,就直接返回 null;否则,就在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则返回其 value 值,否则返回 null。 7. 删除操作 HashMap 的删除操作是通过 remove() 方法实现的。具体来说,它会先计算出 key 的哈希值,然后根据哈希值计算出在数组中的位置。然后,在该位置对应的链表(或红黑树)中查找是否存在具有相同 key 的 Entry 对象,如果存在,则将其删除,否则什么也不做。 8. 总结 以上就是 JDK 8 中 HashMap源码详解。需要注意的是,哈希表虽然可以加快查找的速度,但是在处理哈希冲突、扩容等问题上也存在一定的复杂性,因此在使用 HashMap 时需要注意其内部实现细节,以便更好地理解其性能和使用方法。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值