学java的都知道HashMap一向是java基础面试的重点,最近抽时间研究了下jdk1.8的HashMap源码,佩服作者写代码的能力,做了下笔记,希望能对自己之后写代码能借鉴思想,受到启发。
HashMap的底层:数组+链表(jdk7及之前)
数组+链表+红黑树(jdk7及之前)
jdk1.8源码逐行解读:
1.首先new一个HashMap的时候,加载因子loadFactor赋值为0.75,new的时候仅做此操作。
2.put操作,调用putVal方法
putVal方法说明,@1:对key值进行hash操作的结果,@2:key值,@3:value值,@4:如果为true则不更新value值,hashmap用的是false,@5:如果为true表处于创建模式。
3,以第一次put操作为例,此时还没有产生任何数据,来看看流程:
①先定义一个Node数组tab,一个Node元素p,int n i;
②判断table是否为空,table是定义的一个Node数组,第一次未作任何操作,所以为空。
③调用resize()方法,该方法注释如下,是说初始化或将table的size*2
resize()方法分析:
①由于是第一次新增,oldTab为null则orderCap=0,orderThr=0
直接跳到②
②newCap会被赋值默认大小16
newThr会被赋值默认size x 加载因子0.75=12;
③临界值threshold赋值为12。
此处创建一个大小为16的数组,并赋值给table
table=new Node[16]
由于oldTab一直为空,所以resize()最终返回一个长度为16的数组。
④此时n=tab.size()为16,i=(15&hash)也就是4个1和key的hash值做与运算,相当于只取低四位,得出tab[i]位置的元素,此时因为是第一次添加tab[i]一定为空,所以直接走newNode方法然后放到tab[i]处。
⑤后续的方法基本都没有什么执行了。hashMap的第一次put完成
4,以后续put操作为例,此时已经存在了一些数据,来看看流程:
①如果已经存在数据了,则直接走①的判断,如果tab[i]不为空,则直接将新元素放置tab[i]处。
②如果tab[i]已经存在了数据,且存在的数据hash值和key的hash值相等,且key值和tab[i]equals为true,将e赋值为tab[i];(即:如果是hash值相等且equals相等则才可以说明是相等。)
③如果不相等,判断tab[i]处元素是否是红黑树,是的话,调用红黑树put的方法。
④开始循环链表,如果下一个元素为空,则newNode到下一个节点。
⑤如果链表的长度>=8了则调用treeifyBin方法
treeifyBin方法分析:
①首先会先判断数组长度是否小于64,如果小于先调用resize()做扩容操作。
②否则将链表转为红黑树(也就是说红黑树必须在链表长度大于8,数组长度小于64才会出现)
⑥继续循环链表,如果下一个元素不为空,且和加入元素相等则跳出。
⑦如果添加的key值存在相等的那么将value替换掉。
⑧afterNodeAccess此函数在HashMap中为空方法。
⑨put操作的最后一步如果本次put有新增元素size++如果大于临界值了,调用resize进行扩容操作。
最后再分析下resize的操作:
①当table不为空时,oldCap>0且oldCap比容器最大值还大的话,直接返回原数组并将临界值置为最大int值。
②当table不为空时,oldCap的两倍还小于容器最大值且oldCap大于容器默认大小16则将容器大小和临界值都扩大两倍。
③当调用带参构造器时会进入改条件。
④初始化时进入该条件。
⑤剩余的操作就是新建一个大小两倍的数组,然后将之前数组中的元素重新计算hash值然后放入。
5,HashMap源码其他部分相关了解
带参构造器:
我们在用 HashMap 的时候,如果用默认构造器,就会建一个初始容量为 16,加载因子为 0.75 的 HashMap。这样做有个缺点,就是在数据量比较大的时候,会进行频繁的扩容操作,扩容会发生数据的移位,为了避免扩容,提高性能,我们习惯预估下容量,然后通过带容量的构造器创建
hash()方法:
首先我们知道 HashMap 在做 put 操作的时候,会先对 key 做 hash 操作,直接定位到源码位置:
这个操作是把 key 的 hashCode 值与 hashCode 值右移 16 位做异或(不同为 1,相同为 0),这样就是把哈希值的高位和低位一起混合计算,这样就能使生成的 hash 值更离散