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)的时候,就需要我们用链表来解决了。
我们把对应的元素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]
}
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)
视频教程