HashMap
哈希表哈希函数冲突解决位运算符HashMap简介继承关系成员变量DEFAULT_INITIAL_CAPACITY(默认集合初始容量)DEFAULT_LOAD_FACTORTREEIFY_THRESHOLDtablthshold添加元素流程源码tifyBin方法扩容机制流程源码并发问题
默认JDK8,如果有错误,麻烦在评论区告诉我一下
哈希表
哈希表(Hashtabl,也叫散列表),能够根据键(ky)直接访问相应的值(valu)。查找算法分为两步
/p>
用哈希函数将被查找的ky转化为数组的一个索引。理想情况下,不同的ky都会转化为不同的索引值,但现实情况也会存在不同的ky有相同的索引值。这就叫碰撞冲突,可以通过拉链法或线性探测法解决冲突(后面会讲)。通过生成的索引值直接访问数据。
哈希表是算法在时间和空间上做出权衡的经典案例。如果没有内存限制,那么我们可以直接将ky作为数组的索引,那么所有的查找操作只需要访问内存一次;如果没有时间限制,我们可以用无序数组并进行顺序查找,这样只需要很少的内存,但花费的时间太长了。而哈希表则使用了适度的空间和时间并在这两种极端中找到一个平衡。
哈希函数
假设有一个能够保存M个键值对的数组,那么我们就需要一个能够将任意ky转化为该数组范围内的索引([0,M-]范围内的整数)的哈希函数。这个哈希函数最好还要满足下面的要求
/p>
容易计算并且速度快。计算出来的索引值最好能够均匀分布所有的键,避免产生太多的冲突。
由于ky的类型可能是数字,字符串,所以我们需要设计不同的哈希函数来对应不同的键。Java中为许多常用的数据类型重写了hashCod()方法,在文章Objct中的hashCod()终于搞懂了!!!有详细的介绍。
冲突解决
当出现两个或多个ky的哈希值相同的情况,可以通过拉链法或线性探测法来解决,这篇文章着重讲解一下拉链法,因为HashMap的底层实现就是基于拉链法。大致思路如下:将大小为M的数组中的每个元素指向一个链表,链表中的每个结点都存储了哈希值为该元素的索引的键值对。当要查找某个元素时,首先根据哈希值找到对应的链表,然后沿着链表顺序查找相应的ky,并返回valu。
位运算符
在介绍HashMap之前补充一些位运算符的知识
:左移,空位补0,被移除的高位丢弃,空缺位补0。:右移,被移位的二进制最高位是0,右移后,空缺位补0;最高位是,空缺位补。:无符号右移,被移位二进制最高位无论是0或者是,空缺位都用0补。:与运算,二进制位进行运算,只有时结果是,否则是0;
:或运算,二进制位进行
运算,只有0
0时结果是0,否则是;^:异或运算,相同二进制位进行^运算,结果是0;^=0,0^0=0;不相同二进制位^运算结果是。^0=,0^=~:取反运算,正数取反,各二进制码按补码各位取反负数取反,各二进制码按补码各位取反
HashMap简介
HashMap基于哈希表的Map接口实现,是以ky-valu存储形式存在,即主要用来存放键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。它的ky、valu都可以为null。此外,HashMap中的映射不是有序的。
JDK.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCod方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突)。
JDK.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考tifyBin方法。
虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。
继承关系
HashMap类实现了一个Map接口,该接口定义了一组键值对映射通用的操作,同时还继承了AbstractMap抽象类,该抽象类继承Map接口。但是HashMap类通过继承AbstractMap抽象类也实现了Map接口,是不是多此一举呀?
据Java集合框架的创始人JoshBloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。StackOvrFlow问题链接
HashMap还实现了Clonabl接口和Srializabl接口,分别用来进行对象克隆和对象序列化。
成员变量
下面介绍几个重要的变量
DEFAULT_INITIAL_CAPACITY(默认集合初始容量)
//默认初始容量是6(必须是的n次幂)staticfinalintDEFAULT_INITIAL_CAPACITY=4;//aka6
可以发现初始容量必须是的n次幂,具体原因下面分析一下:根据哈希表的原理,我们可以知道当向HashMap中添加一个元素时,需要根据ky的哈希值去确定其在数组中的具体位置。HashMap为了存取高效,要尽量减少冲突碰撞,也就是要尽量把数据分配均匀,每个链表长度大致相同。ky的哈希值可能是一个很大的整数,超过散列表的范围,因此需要把它缩小到适合索引的范围。假设假设哈希表的索引处于0到n-之间,将一个整数缩小到0和n-之间的最常用的做法是使用hash%n,其中n为大于的素数。比如Hashtabl初始化桶大小为,就是桶大小设计为素数的应用(Hashtabl扩容后不能保证还是素数)。理想情况下应该为n选择一个素数,但是选择一个大的素数将很耗时。而在HashMap中使用的方法很巧妙,它通过hash(lngth-)来计算,当lngth长度为的n次幂时,hash(lngth-)的运算结果总是等价于hash%lngth,的运算效率也比%更高。在创建HashMap对象的时候,可以传入initialCapacity,如果输入的大小不是的n次幂,那么底层会通过位移运算和或运算得到一个离你传入的数字最近的一个的幂次数。源码如下
/p>
//创建HashMap集合的对象,指定数组长度是0,不是的幂HashMaphashMap=nwHashMap(0);-----------------------------------------------/*指定“容量大小”和“加载因子”的构造函数initialCapacity:指定的容量loadFactor:指定的加载因子*/publicHashMap(intinitialCapacity,floatloadFactor){ //判断初始化容量initialCapacity是否小于0if(initialCapacity0)//如果小于0,则抛出非法的参数异常IllgalArgumntExcptionthrownwIllgalArgumntExcption("Illgalinitialcapacity:"+initialCapacity); //判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY-》的0次幂if(initialCapacityMAXIMUM_CAPACITY)//如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacityinitialCapacity=MAXIMUM_CAPACITY; //判断负载因子loadFactor是否小于等于0或者是否是一个非数值if(loadFactor=0
Float.isNaN(loadFactor))//如果满足上述其中之一,则抛出非法的参数异常IllgalArgumntExcptionthrownwIllgalArgumntExcption("Illgalloadfactor:"+loadFactor); //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactorthis.loadFactor=loadFactor; /***tablSizFor(initialCapacity)判断指定的初始化容量是否是的n次幂,如果不是那么会*变为比指定初始化容量大的最小的的n次幂。但是注意,在tablSizFor方法体内部将计算后的数据返回给调用这里了,*并且直接赋值给thshold边界值了。有些人会觉得这里是一个bug,应该这样书写:*this.thshold=tablSizFor(initialCapacity)*this.loadFactor;*这样才符合thshold的意思(当HashMap的siz到达thshold这个阈值时会扩容)。*但是,请注意,在jdk8以后的构造方法中,并没有对tabl这个成员变量进行初始化,*tabl的初始化被推迟到了put方法中,在put方法中会对thshold重新计算,put方法的具体实现我们下面会进行讲解*/this.thshold=tablSizFor(initialCapacity);}最后调用了tablSizFor,来看一下方法实现:/***Rturnsapowroftwosizforthgivntargtcapacity.返回比指定初始化容量大的最小的的n次幂*/staticfinalinttablSizFor(intcap){intn=cap-;n
=n;n
=n;n
=n4;n
=n8;n
=n6;turn(n0)?
n=MAXIMUM_CAPACITY)?MAXIMUM_CAPACITY:n+;}
tablSizFor(intcap)是解决initialCapacity不是的n次幂的核心方法。过程如下图:这里说两个问题:
首先,为什么要对cap做减操作,intn=cap-?这是为了防止,cap已经是的幂。如果cap已经是的幂,又没有执行这个减操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的倍。比如传入6,如果不减,最后结果会是,读者可以进行测试。容量最大也就是bit的正数,因此最后n
=n6,最多也就个,但是这已经是负数了。在执行tablSizFor之前,其实会对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(^0),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(^0),会执行移位操作。所以这里面的移位操作之后,最大0个,不会大于等于MAXIMUM_CAPACITY。0个,加之后得^0。
DEFAULT_LOAD_FACTOR
staticfinalfloatDEFAULT_LOAD_FACTOR=0.75f;
默认的装载因子,用于衡量HashMap满的程度,计算HashMap的实时装载因子loadFactor的方法是siz/capacity。siz是当前HashMap中存放键值对的数量,capacity是桶的数量。默认值为0.75,loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。这是对空间和时间效率的一个平衡选择,建议不要修改。当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,而扩容这个过程涉及到hash、复制数据等操作,非常消耗性能。,所以开发中尽量减少扩容的次数,可以通过创建HashMap集合对象时指定初始容量来尽量避免。
TREEIFY_THRESHOLD
//当桶(buckt)上的结点数大于这个值时会转成红黑树staticfinalintTREEIFY_THRESHOLD=8;
TNods占用空间是普通Nods的两倍,所以只有当bin包含足够多的节点时才会转成TNods,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,数组长度大于64且链表长度达到8就转成红黑树,当长度降到6就转成普通bin。
//当桶(buckt)上的结点数小于这个值时树转链表staticfinalintUNTREEIFY_THRESHOLD=6;//当集合中的容量大于这个值时,表中的桶才会进行树形化,否则桶内元素太多时会扩容。staticfinalintMIN_TREEIFY_CAPACITY=64;
这样就解释了为什么不是一开始就将其转换为TNods,而是需要一定节点数才转为TNods,其实就是空间和时间的权衡。
这段内容还说到:当hashCod离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCod下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCod算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.,几乎是不可能事件。所以,之所以选择8,不是随便决定的,而是根据概率统计决定的。由此可见,发展将近0年的Java每一项改动和优化都是非常严谨和科学的。
也就是说:选择8因为符合泊松分布,超过8的时候,概率已经非常小了,所以我们选择8这个数字。
tabl
//存储元素的数组transintNodK,V[]tabl;
在JDK8中我们了解到HashMap是由数组加链表加红黑树来组成的结构,其中tabl就是HashMap中的数组,JDK8之前数组类型是EntryK,V类型。从JDK8之后是NodK,V类型。只是换了个名字,都实现了一样的接口:Map.EntryK,V。负责存储键值对数据的。
thshold
//临界值当实际大小(容量*负载因子)超过临界值时,会进行扩容intthshold;
计算公式:capacity(数组长度默认6)*loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。当siz=thshold的时候,那么就要考虑对数组的siz(扩容),也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。扩容后的HashMap容量是之前容量的两倍。
添加元素
流程
put方法还是比较复杂的,步骤大致如下
/p>
先通过hash值计算出ky映射到哪个桶;
如果桶上没有碰撞冲突,则直接插入;
如果出现碰撞冲突了,则需要处理冲突:
如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
如果桶中存在重复的键,则为该键替换新值valu;
如果siz大于阈值thshold,则进行扩容;
源码
具体的方法如下:
publicVput(Kky,Vvalu){turnputVal(hash(ky),ky,valu,fals,tru);}
我们可以看到在putVal()方法中ky在这里执行了一下hash()方法。确定哈希桶数组索引位置需要下面三步
/p>
取hashCod值,ky.hashCod()。高位参与运算,h6。取模运算,(n-)hash。
来看一下Hash方法是如何实现的。
staticfinalinthash(Objctky){inth; /*)如果ky等于null:可以看到当ky等于null的时候也是有哈希值的,返回的是0.)如果ky不等于null:首先计算出ky的hashCod赋值给h,然后与h无符号右移6位后的二进制进行按位异或得到最后的hash值*/turn(ky==null)?0
h=ky.hashCod())^(h6);}
hashCod()得到的是一个位int类型的值,通过hashCod()的高6位异或低6位实现,如果当n即数组长度很小,假设是6的话,那么n-即为,这样的值和hashCod()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。例如下面这个例子,hashCod值的红色部分再怎么变化也没用,如果hashCod值的红色部分变了,黑色部分没变,就会造成索引结果仍然是0,从而造成了哈希冲突。
获取桶数组索引的步骤如下:现在看putVal()方法,看看它到底做了什么。主要参数:
hash:ky的hash值ky:原始Kyvalu:要存放的值onlyIfAbsnt:如果tru代表不更改现有的值vict:如果为fals表示tabl为创建状态
finalVputVal(inthash,Kky,Vvalu,boolanonlyIfAbsnt,boolanvict){NodK,V[]tab;NodK,Vp;intn,i;/*)transintNodK,V[]tabl;表示存储Map集合中元素的数组。)(tab=tabl)==null表示将空的tabl赋值给tab,然后判断tab是否等于null,第一次肯定是null)(n=tab.lngth)==0表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0由于if判断使用双或,满足一个即可,则执行代码n=(tab=siz()).lngth;进行数组初始化。并将初始化好的数组长度赋值给n.4)执行完n=(tab=siz()).lngth,数组tab每个空间都是null*/if((tab=tabl)==null
(n=tab.lngth)==0)n=(tab=siz()).lngth;/*)i=(n-)hash表示计算数组的索引赋值给i,即确定元素存放在哪个桶中)p=tab[i=(n-)hash]表示获取计算出的位置的数据赋值给节点p)(p=tab[i=(n-)hash])==null判断节点位置是否等于null,如果为null,则执行代码:tab=nwNod(hash,ky,valu,null);根据键值对创建新的节点放入该位置的桶中小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置*/if((p=tab[i=(n-)hash])==null)//创建一个新的节点存入到桶中tab=nwNod(hash,ky,valu,null);ls{//执行ls说明tab不等于null,表示这个位置已经有值了。NodK,V;Kk;/*比较桶中第一个元素(数组中的结点)的hash值和ky是否相等)p.hash==hash:p.hash表示原来存在数据的hash值hash表示后添加数据的hash值比较两个hash值是否相等说明:p表示tab,即nwNod(hash,ky,valu,null)方法返回的Nod对象。NodK,VnwNod(inthash,Kky,Vvalu,NodK,Vnxt){turnnwNod(hash,ky,valu,nxt);}而在Nod类中具有成员变量hash用来记录着之前数据的hash值的)(k=p.ky)==ky:p.ky获取原来数据的ky赋值给kky表示后添加数据的ky比较两个ky的地址值是否相等)ky!=nullky.quals(k):能够执行到这里说明两个ky的地址值不相等,那么先判断后添加的ky是否等于null,如果不等于null再调用quals方法判断两个ky的内容是否相等*/if(p.hash==hash((k=p.ky)==ky
(ky!=nullky.quals(k))))/*说明:两个元素哈希值相等,并且ky的值也相等将旧的元素整体对象赋值给,用来记录*/=p;//hash值不相等或者ky不相等;判断p是否为红黑树结点lsif(pinstancofTNod)//放入树中=((TNodK,V)p).putTVal(this,tab,hash,ky,valu);//说明是链表节点ls{/*)如果是链表的话需要遍历到最后节点然后插入)采用循环遍历的方式,判断链表中是否有重复的ky*/for(intbinCount=0;;++binCount){/*)=p.nxt获取p的下一个元素赋值给)(=p.nxt)==null判断p.nxt是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的ky,则说明HashMap没有包含该键将该键值对插入链表中*/if((=p.nxt)==null){/*)创建一个新的节点插入到尾部p.nxt=nwNod(hash,ky,valu,null);NodK,VnwNod(inthash,Kky,Vvalu,NodK,Vnxt){turnnwNod(hash,ky,valu,nxt);}注意第四个参数nxt是null,因为当前元素插入到链表末尾了,那么下一个节点肯定是null)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素*/p.nxt=nwNod(hash,ky,valu,null);/*)节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树)intbinCount=0:表示for循环的初始化值。从0开始计数。记录着遍历节点的个数。值是0表示第一个节点,表示第二个节点。。。。7表示第八个节点,加上数组中的的一个元素,元素个数是9TREEIFY_THRESHOLD---》8----》7如果binCount的值是7(加上数组中的的一个元素,元素个数是9)TREEIFY_THRESHOLD-也是7,此时转换红黑树*/if(binCount=TREEIFY_THRESHOLD-)//-forst//转换为红黑树tifyBin(tab,hash);//跳出循环bak;}/*执行到这里说明=p.nxt不是null,不是最后一个元素。继续判断链表中结点的ky值与插入的元素的ky值是否相等*/if(.hash==hash((k=.ky)==ky
(ky!=nullky.quals(k))))//相等,跳出循环/*要添加的元素和链表中的存在的元素的ky相等了,则跳出for循环。不用再继续比较了直接执行下面的if语句去替换去if(!=null)*/bak;/*说明新添加的元素和当前节点不相等,继续查找下一个节点。用于遍历桶中的链表,与前面的=p.nxt组合,可以遍历链表*/p=;}}/*表示在桶中找到ky值、hash值与插入元素相等的结点也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值这里完成了put方法的修改功能*/if(!=null){//记录的valuVoldValu=.valu;//onlyIfAbsnt为fals或者旧值为nullif(!onlyIfAbsnt
oldValu==null)//用新值替换旧值//.valu表示旧值valu表示新值.valu=valu;//访问后回调aftrNodAccss();//返回旧值turnoldValu;}}//修改记录次数++modCount;//判断实际大小是否大于thshold阈值,如果超过则扩容if(++sizthshold)siz();//插入后回调aftrNodInsrtion(vict);turnnull;}
tifyBin方法
节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树(tifyBin方法里面还会判断数组长度是否大于64),转换红黑树的方法tifyBin,整体代码如下:
if(binCount=TREEIFY_THRESHOLD-)//-forst//转换为红黑树tab表示数组名hash表示哈希值tifyBin(tab,hash);
tifyBin方法如下所示:
/***Rplacsalllinkdnodsinbinatindxforgivnhashunlss*tablistoosmall,inwhichcassizsinstad.替换指定哈希表的索引处桶中的所有链接节点,除非表太小,否则将修改大小。NodK,V[]tab=tab数组名inthash=hash表示哈希值*/finalvoidtifyBin(NodK,V[]tab,inthash){intn,indx;NodK,V;/*如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY=64),就去扩容。而不是将节点变为红黑树。目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。这时进行扩容,那么重新计算哈希值,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。*/if(tab==null
(n=tab.lngth)MIN_TREEIFY_CAPACITY)//扩容方法siz();lsif((=tab[indx=(n-)hash])!=null){/*)执行到这里说明哈希表中的数组长度大于阈值64,开始进行树形化)=tab[indx=(n-)hash]表示将数组中的元素取出赋值给,是哈希表中指定位置桶里的链表节点,从第一个开始*///hd:红黑树的头结点tl:红黑树的尾结点TNodK,Vhd=null,tl=null;do{//新创建一个树的节点,内容和当前链表节点一致TNodK,Vp=placmntTNod(,null);if(tl==null)//将新创键的p节点赋值给红黑树的头结点hd=p;ls{/*p.pv=tl:将上一个节点p赋值给现在的p的前一个节点tl.nxt=p;将现在节点p作为树的尾结点的下一个节点*/p.pv=tl;tl.nxt=p;}tl=p;/*=.nxt将当前节点的下一个节点赋值给,如果下一个节点不等于null则回到上面继续取出链表中节点转换为红黑树*/}whil((=.nxt)!=null);/*让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了*/if((tab[indx]=hd)!=null)hd.tify(tab);}}
扩容机制
流程
当HashMap中的元素个数超过capacity(数组长度默认6)*loadFactor(负载因子默认0.75时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为6,那么当HashMap中的元素个数超过6×0.75=(这个值就是阈值或者边界值thshold值)的时候,就把数组的大小扩展为×6=,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。HashMap在进行扩容时,使用的hash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的(n-)hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。例如我们从6扩展为时,具体的变化如下所示:容量变为原来的二倍后,n-的二进制数也由变为。当容量为6时,(n-)hash的结果都是00,即十进制的5;当容量变为时,hash的结果是00,十进制的5,hash的结果是00,十进制的(5+6)。扩容之后所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是还是0就可以了,是0的话索引没变,是的话索引变成“原索引+oldCap(原位置+旧容量)”。在源码中通过.hasholdCap进行判断。oldCap就是扩容之前的容量,在上面的例子中就是6,hash结果为0,表示还在原位置,hash结果不为,表示结果为原位置+旧容量,即5+6=。可以看看下图为6扩充为的siz示意图:正是因为这样巧妙的hash方式,既省去了重新计算hash值的时间,而且同时,由于新增的bit是0还是可以认为是随机的,在siz的过程中保证了hash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了hash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。
源码
finalNodK,V[]siz(){//得到当前数组NodK,V[]oldTab=tabl;//如果当前数组等于null长度返回0,否则返回当前数组的长度intoldCap=(oldTab==null)?0
ldTab.lngth;//当前阈值点默认是(6*0.75)intoldThr=thshold;intnwCap,nwThr=0;//如果老的数组长度大于0//开始计算扩容后的大小if(oldCap0){//超过最大值就不再扩充了,就只好随你碰撞去吧if(oldCap=MAXIMUM_CAPACITY){//修改阈值为int的最大值thshold=Intgr.MAX_VALUE;turnoldTab;}/*没超过最大值,就扩充为原来的倍)(nwCap=oldCap)MAXIMUM_CAPACITY扩大到倍之后容量要小于最大容量)oldCap=DEFAULT_INITIAL_CAPACITY原数组长度大于等于数组初始化长度6*/lsif((nwCap=oldCap)MAXIMUM_CAPACITYoldCap=DEFAULT_INITIAL_CAPACITY)//阈值扩大一倍nwThr=oldThr;//doublthshold}//老阈值点大于0直接赋值lsif(oldThr0)//老阈值赋值给新的数组长度nwCap=oldThr;ls{//直接使用默认值nwCap=DEFAULT_INITIAL_CAPACITY;//6nwThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);}//计算新的siz最大上限if(nwThr==0){floatft=(float)nwCap*loadFactor;nwThr=(nwCapMAXIMUM_CAPACITYft(float)MAXIMUM_CAPACITY?(int)ft:Intgr.MAX_VALUE);}//新的阈值默认原来是乘以之后变为4thshold=nwThr;//创建新的哈希表
SuppssWarnings({"rawtyps","unchckd"})//nwCap是新的数组长度--》NodK,V[]nwTab=(NodK,V[])nwNod[nwCap];tabl=nwTab;//判断旧数组是否等于空if(oldTab!=null){//把每个buckt都移动到新的buckts中//遍历旧的哈希表的每个桶,重新计算桶里元素的新位置for(intj=0;joldCap;++j){NodK,V;if((=oldTab[j])!=null){//原来的数据赋值为null便于GC回收oldTab[j]=null;//判断数组是否有下一个引用if(.nxt==null)//没有下一个引用,说明不是链表,当前桶上只有一个键值对,直接插入nwTab[.hash(nwCap-)]=;//判断是否是红黑树lsif(instancofTNod)//说明是红黑树来处理冲突的,则调用相关方法把树分开((TNodK,V)).split(this,nwTab,j,oldCap);ls{//采用链表处理冲突NodK,VloHad=null,loTail=null;NodK,VhiHad=null,hiTail=null;NodK,Vnxt;//通过上述讲解的原理来计算节点的新位置do{//原索引nxt=.nxt; //这里来判断如果等于tru这个节点在siz之后不需要移动位置if((.hasholdCap)==0){if(loTail==null)loHad=;lsloTail.nxt=;loTail=;}//原索引+oldCapls{if(hiTail==null)hiHad=;lshiTail.nxt=;hiTail=;}}whil((=nxt)!=null);//原索引放到buckt里if(loTail!=null){loTail.nxt=null;nwTab[j]=loHad;}//原索引+oldCap放到buckt里if(hiTail!=null){hiTail.nxt=null;nwTab[j+oldCap]=hiHad;}}}}}turnnwTab;}
好,源码就先分析到这里,如果前面看懂了,后面的删除查找对你来说相信也不是问题。
并发问题
在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurntHashMap。在多线程环境下,JDK.7会产生死循环、数据丢失、数据覆盖的问题,JDK.8中会有数据覆盖和多线程同时扩容等问题。
,