在Java8与Java7中HashMap源码实现的对比
一、HashMap的原理介绍
此乃老生常谈,不作仔细解说。
一句话概括之:HashMap是一个散列表,它存储的内容是键值对(key-value)映射。
二、Java7中HashMap的源码分析
首先是HashMap的构造函数代码块1中,根据初始化的Capacity与loadFactor(加载因子)初始化HashMap.
//代码块1
publicHashMap(intinitialCapacity,floatloadFactor){
if(initialCapacity<0)
thrownewIllegalArgumentException("Illegalinitialcapacity:"+
initialCapacity);
if(initialCapacity>MAXIMUM_CAPACITY)
initialCapacity=MAXIMUM_CAPACITY;
if(loadFactor<=0||Float.isNaN(loadFactor))
thrownewIllegalArgumentException("Illegalloadfactor:"+loadFactor);
this.loadFactor=loadFactor;
threshold=initialCapacity;
init();
}
Java7中对于<key1,value1>的put方法实现相对比较简单,首先根据key1的key值计算hash值,再根据该hash值与table的length确定该key所在的index,如果当前位置的Entry不为null,则在该Entry链中遍历,如果找到hash值和key值都相同,则将值value覆盖,返回oldValue;如果当前位置的Entry为null,则直接addEntry。
代码块2
publicVput(Kkey,Vvalue){
if(table==EMPTY_TABLE){
inflateTable(threshold);
}
if(key==null)
returnputForNullKey(value);
inthash=hash(key);
inti=indexFor(hash,table.length);
for(Entry<K,V>e=table[i];e!=null;e=e.next){
Objectk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
returnnull;
}
//addEntry方法中会检查当前table是否需要resize
voidaddEntry(inthash,Kkey,Vvalue,intbucketIndex){
if((size>=threshold)&&(null!=table[bucketIndex])){
resize(2*table.length);//当前map中的size如果大于threshole的阈值,则将resize将table的length扩大2倍。
hash=(null!=key)?hash(key):0;
bucketIndex=indexFor(hash,table.length);
}
createEntry(hash,key,value,bucketIndex);
}
Java7中resize()方法的实现比较简单,将OldTable的长度扩展,并且将oldTable中的Entry根据rehash的标记重新计算hash值和index移动到newTable中去。
代码如代码块3中所示,
//代码块3--JDK7中HashMap.resize()方法
voidresize(intnewCapacity){
Entry[]oldTable=table;
intoldCapacity=oldTable.length;
if(oldCapacity==MAXIMUM_CAPACITY){
threshold=Integer.MAX_VALUE;
return;
}
Entry[]newTable=newEntry[newCapacity];
transfer(newTable,initHashSeedAsNeeded(newCapacity));
table=newTable;
threshold=(int)Math.min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1);
}
/**
*将当前table的Entry转移到新的table中
*/
voidtransfer(Entry[]newTable,booleanrehash){
intnewCapacity=newTable.length;
for(Entry<K,V>e:table){
while(null!=e){
Entry<K,V>next=e.next;
if(rehash){
e.hash=null==e.key?0:hash(e.key);
}
inti=indexFor(e.hash,newCapacity);
e.next=newTable[i];
newTable[i]=e;
e=next;
}
}
}
HashMap性能的有两个参数:初始容量(initialCapacity)和加载因子(loadFactor)。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行rehash操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
根据源码分析可以看出:在Java7中HashMap的entry是按照index索引存储的,遇到hash冲突的时候采用拉链法解决冲突,将冲突的key和value插入到链表list中。
然而这种解决方法会有一个缺点,假如key值都冲突,HashMap会退化成一个链表,get的复杂度会变成O(n)。
在Java8中为了优化该最坏情况下的性能,采用了平衡树来存放这些hash冲突的键值对,性能由此可以提升至O(logn)。
代码块4--JDK8中HashMap中常量定义 staticfinalintDEFAULT_INITIAL_CAPACITY=1<<4; staticfinalintTREEIFY_THRESHOLD=8;//是否将list转换成tree的阈值 staticfinalintUNTREEIFY_THRESHOLD=6;//在resize操作中,决定是否untreeify的阈值 staticfinalintMIN_TREEIFY_CAPACITY=64;//决定是否转换成tree的最小容量 staticfinalfloatDEFAULT_LOAD_FACTOR=0.75f;//default的加载因子
在Java8HashMap的put方法实现如代码块5所示,
代码块5--JDK8HashMap.put方法
publicVput(Kkey,Vvalue){
returnputVal(hash(key),key,value,false,true);
}
finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,
booleanevict){
Node<K,V>[]tab;Node<K,V>p;intn,i;
if((tab=table)==null||(n=tab.length)==0)
n=(tab=resize()).length;//table为空的时候,n为table的长度
if((p=tab[i=(n-1)&hash])==null)
tab[i]=newNode(hash,key,value,null);//(n-1)&hash与Java7中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。
else{
//若i位置上的值不为空,判断当前位置上的Nodep是否与要插入的key的hash和key相同
Node<K,V>e;Kk;
if(p.hash==hash&&
((k=p.key)==key||(key!=null&&key.equals(k))))
e=p;//相同则覆盖之
elseif(pinstanceofTreeNode)
//不同,且当前位置上的的nodep已经是TreeNode的实例,则再该树上插入新的node。
e=((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value);
else{
//在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。
for(intbinCount=0;;++binCount){
if((e=p.next)==null){
p.next=newNode(hash,key,value,null);
if(binCount>=TREEIFY_THRESHOLD-1)//-1for1st
treeifyBin(tab,hash);
break;
}
if(e.hash==hash&&
((k=e.key)==key||(key!=null&&key.equals(k))))
break;
p=e;
}
}
if(e!=null){//existingmappingforkey
VoldValue=e.value;
if(!onlyIfAbsent||oldValue==null)
e.value=value;
afterNodeAccess(e);
returnoldValue;
}
}
++modCount;
if(++size>threshold)
resize();
afterNodeInsertion(evict);
returnnull;
}
再看下resize方法,由于需要考虑hash冲突解决时采用的可能是list也可能是balancetree的方式,因此resize方法相比JDK7中复杂了一些,
代码块6--JDK8的resize方法
inalNode<K,V>[]resize(){
Node<K,V>[]oldTab=table;
intoldCap=(oldTab==null)?0:oldTab.length;
intoldThr=threshold;
intnewCap,newThr=0;
if(oldCap>0){
if(oldCap>=MAXIMUM_CAPACITY){
threshold=Integer.MAX_VALUE;//如果超过最大容量,无法再扩充table
returnoldTab;
}
elseif((newCap=oldCap<<1)<MAXIMUM_CAPACITY&&
oldCap>=DEFAULT_INITIAL_CAPACITY)
newThr=oldThr<<1;//threshold门槛扩大至2倍
}
elseif(oldThr>0)//initialcapacitywasplacedinthreshold
newCap=oldThr;
else{//zeroinitialthresholdsignifiesusingdefaults
newCap=DEFAULT_INITIAL_CAPACITY;
newThr=(int)(DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY);
}
if(newThr==0){
floatft=(float)newCap*loadFactor;
newThr=(newCap<MAXIMUM_CAPACITY&&ft<(float)MAXIMUM_CAPACITY?
(int)ft:Integer.MAX_VALUE);
}
threshold=newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[]newTab=(Node<K,V>[])newNode[newCap];//创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。
table=newTab;
if(oldTab!=null){
for(intj=0;j<oldCap;++j){
Node<K,V>e;
if((e=oldTab[j])!=null){
oldTab[j]=null;
if(e.next==null)
newTab[e.hash&(newCap-1)]=e;
elseif(einstanceofTreeNode)
((TreeNode<K,V>)e).split(this,newTab,j,oldCap);
//split方法会将树分割为lower和uppertree两个树,
如果子树的节点数小于了UNTREEIFY_THRESHOLD阈值,则将树untreeify,将节点都存放在newTab中。
else{//preserveorder
Node<K,V>loHead=null,loTail=null;
Node<K,V>hiHead=null,hiTail=null;
Node<K,V>next;
do{
next=e.next;
if((e.hash&oldCap)==0){
if(loTail==null)
loHead=e;
else
loTail.next=e;
loTail=e;
}
else{
if(hiTail==null)
hiHead=e;
else
hiTail.next=e;
hiTail=e;
}
}while((e=next)!=null);
if(loTail!=null){
loTail.next=null;
newTab[j]=loHead;
}
if(hiTail!=null){
hiTail.next=null;
newTab[j+oldCap]=hiHead;
}
}
}
}
}
returnnewTab;
}
再看一下tree的treeifyBin方法和putTreeVal方法的实现,底层采用了红黑树的方法。
//代码块7
//MIN_TREEIFY_CAPACITY的值为64,若当前table的length不够,则resize()
finalvoidtreeifyBin(Node<K,V>[]tab,inthash){
intn,index;Node<K,V>e;
if(tab==null||(n=tab.length)<MIN_TREEIFY_CAPACITY)
resize();
elseif((e=tab[index=(n-1)&hash])!=null){
TreeNode<K,V>hd=null,tl=null;
do{
TreeNode<K,V>p=replacementTreeNode(e,null);
if(tl==null)
hd=p;
else{
p.prev=tl;
tl.next=p;
}
tl=p;
}while((e=e.next)!=null);
if((tab[index]=hd)!=null)
hd.treeify(tab);
}
}
//putVal的tree版本
finalTreeNode<K,V>putTreeVal(HashMap<K,V>map,Node<K,V>[]tab,
inth,Kk,Vv){
Class<?>kc=null;
booleansearched=false;
TreeNode<K,V>root=(parent!=null)?root():this;
for(TreeNode<K,V>p=root;;){
intdir,ph;Kpk;
if((ph=p.hash)>h)
dir=-1;
elseif(ph<h)
dir=1;
elseif((pk=p.key)==k||(k!=null&&k.equals(pk)))
returnp;
elseif((kc==null&&
(kc=comparableClassFor(k))==null)||
(dir=compareComparables(kc,k,pk))==0){
if(!searched){
TreeNode<K,V>q,ch;
searched=true;
if(((ch=p.left)!=null&&
(q=ch.find(h,k,kc))!=null)||
((ch=p.right)!=null&&
(q=ch.find(h,k,kc))!=null))
returnq;
}
dir=tieBreakOrder(k,pk);
}
TreeNode<K,V>xp=p;
if((p=(dir<=0)?p.left:p.right)==null){
Node<K,V>xpn=xp.next;
TreeNode<K,V>x=map.newTreeNode(h,k,v,xpn);
if(dir<=0)
xp.left=x;
else
xp.right=x;
xp.next=x;
x.parent=x.prev=xp;
if(xpn!=null)
((TreeNode<K,V>)xpn).prev=x;
moveRootToFront(tab,balanceInsertion(root,x));
returnnull;
}
}
}
看了这些源码,并一一做了比较之后,惊叹于源码之妙,收益良多。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。