HashMap剖析

Java集合:HashMap源碼剖析


一、HashMap概述

二、HashMap的數(shù)據(jù)結(jié)構(gòu)

三、HashMap源碼分析

1、關(guān)鍵屬性

2、構(gòu)造方法

3、存儲數(shù)據(jù)

4、調(diào)整大小

5、數(shù)據(jù)讀取

?6、HashMap的性能參數(shù)

?7、Fail-Fast機(jī)制


一、HashMap概述

HashMap基于哈希表的?Map?接口的實(shí)現(xiàn)。此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用?null?值和?null?鍵。(除了不同步和允許使用?null?之外,HashMap?類與?Hashtable?大致相同。)此類不保證映射的順序,特別是它不保證該順序恒久不變。

  值得注意的是HashMap不是線程安全的,如果想要線程安全的HashMap,可以通過Collections類的靜態(tài)方法synchronizedMap獲得線程安全的HashMap。

Map map = Collections.synchronizedMap(newHashMap());


二、HashMap的數(shù)據(jù)結(jié)構(gòu)

HashMap的底層主要是基于數(shù)組和鏈表來實(shí)現(xiàn)的,它之所以有相當(dāng)快的查詢速度主要是因?yàn)樗峭ㄟ^計(jì)算散列碼來決定存儲的位置。HashMap中主要是通過key的hashCode來計(jì)算hash值的,只要hashCode相同,計(jì)算出來的hash值就一樣。如果存儲的對象對多了,就有可能不同的對象所算出來的hash值是相同的,這就出現(xiàn)了所謂的hash沖突。學(xué)過數(shù)據(jù)結(jié)構(gòu)的同學(xué)都知道,解決hash沖突的方法有很多,HashMap底層是通過鏈表來解決hash沖突的。


?圖中,0~15部分即代表哈希表,也稱為哈希數(shù)組,數(shù)組的每個(gè)元素都是一個(gè)單鏈表的頭節(jié)點(diǎn),鏈表是用來解決沖突的,如果不同的key映射到了數(shù)組的同一位置處,就將其放入單鏈表中。


從上圖我們可以發(fā)現(xiàn)哈希表是由數(shù)組+鏈表組成的,一個(gè)長度為16的數(shù)組中,每個(gè)元素存儲的是一個(gè)鏈表的頭結(jié)點(diǎn)Bucket。那么這些元素是按照什么樣的規(guī)則存儲到數(shù)組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數(shù)組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數(shù)組下標(biāo)為12的位置。


HashMap其實(shí)也是一個(gè)線性的數(shù)組實(shí)現(xiàn)的,所以可以理解為其存儲數(shù)據(jù)的容器就是一個(gè)線性數(shù)組。這可能讓我們很不解,一個(gè)線性的數(shù)組怎么實(shí)現(xiàn)按鍵值對來存取數(shù)據(jù)呢?這里HashMap有做一些處理。


  首先HashMap里面實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類Entry,其重要的屬性有?key?,?value,?next,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實(shí)現(xiàn)的一個(gè)基礎(chǔ)bean,我們上面說到HashMap的基礎(chǔ)就是一個(gè)線性數(shù)組,這個(gè)數(shù)組就是Entry[],Map里面的內(nèi)容都保存在Entry[]里面。


我們看看HashMap中Entry類的代碼:


/** Entry是單向鏈表。? ?

? ? * 它是 “HashMap鏈?zhǔn)酱鎯Ψā睂?yīng)的鏈表。? ?

? ? *它實(shí)現(xiàn)了Map.Entry 接口,即實(shí)現(xiàn)getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數(shù)?

? ? **/?

? ? static class Entry implements Map.Entry {? ?

? ? ? ? final K key;? ?

? ? ? ? V value;? ?

? ? ? ? // 指向下一個(gè)節(jié)點(diǎn)? ? Entry next;? ?

? ? ? ? final int hash;? ?


? ? ? ? // 構(gòu)造函數(shù)。? ?

? ? ? ? // 輸入?yún)?shù)包括"哈希值(h)", "鍵(k)", "值(v)", "下一節(jié)點(diǎn)(n)"? ? Entry(inth, K k, V v, Entry n) {? ?

? ? ? ? ? ? value = v;? ?

? ? ? ? ? ? next = n;? ?

? ? ? ? ? ? key = k;? ?

? ? ? ? ? ? hash = h;? ?

? ? ? ? }? ?


? ? ? ? public final K getKey() {? ?

? ? ? ? ? ? return key;? ?

? ? ? ? }? ?


? ? ? ? public final V getValue() {? ?

? ? ? ? ? ? return value;? ?

? ? ? ? }? ?


? ? ? ? public final V setValue(V newValue) {? ?

? ? ? ? ? ? V oldValue = value;? ?

? ? ? ? ? ? value = newValue;? ?

? ? ? ? ? ? return oldValue;? ?

? ? ? ? }? ?


? ? ? ? // 判斷兩個(gè)Entry是否相等? ?

? ? ? ? // 若兩個(gè)Entry的“key”和“value”都相等,則返回true。? ?

? ? ? ? // 否則,返回false? ? ? ? ? ? public final boolean equals(Object o) {? ?

? ? ? ? ? ? if(!(o instanceof Map.Entry))? ?

? ? ? ? ? ? ? ? return false;? ?

? ? ? ? ? ? Map.Entry e = (Map.Entry)o;? ?

? ? ? ? ? ? Object k1 = getKey();? ?

? ? ? ? ? ? Object k2 = e.getKey();? ?

? ? ? ? ? ? if(k1 == k2 || (k1 !=null&& k1.equals(k2))) {? ?

? ? ? ? ? ? ? ? Object v1 = getValue();? ?

? ? ? ? ? ? ? ? Object v2 = e.getValue();? ?

? ? ? ? ? ? ? ? if(v1 == v2 || (v1 !=null&& v1.equals(v2)))? ?

? ? ? ? ? ? ? ? ? ? return true;? ?

? ? ? ? ? ? }? ?

? ? ? ? ? ? return false;? ?

? ? ? ? }? ?


? ? ? ? // 實(shí)現(xiàn)hashCode()? ? public finalint hashCode() {? ?

? ? ? ? ? ? return (key==null?0: key.hashCode()) ^? ?

? ? ? ? ? ? ? ? ? (value==null?0 : value.hashCode());? ?

? ? ? ? }? ?


? ? ? ? public final String toString() {? ?

? ? ? ? ? ? return getKey() +"="+ getValue();? ?

? ? ? ? }? ?


? ? ? ? // 當(dāng)向HashMap中添加元素時(shí),會調(diào)用recordAccess()。? ?

? ? ? ? // 這里不做任何處理? ? void recordAccess(HashMap m) {? ?

? ? ? ? }? ?


? ? ? ? // 當(dāng)從HashMap中刪除元素時(shí),會調(diào)用recordRemoval()。? ?

? ? ? ? // 這里不做任何處理? ? void recordRemoval(HashMap m) {? ?

? ? ? ? }? ?

? ? }


HashMap其實(shí)就是一個(gè)Entry數(shù)組,Entry對象中包含了鍵和值,其中next也是一個(gè)Entry對象,它就是用來處理hash沖突的,形成一個(gè)鏈表。


三、HashMap源碼分析


1、關(guān)鍵屬性

  先看看HashMap類中的一些關(guān)鍵屬性:


transient Entry[] table;//存儲元素的實(shí)體數(shù)組transientintsize;//存放元素的個(gè)數(shù)intthreshold;//臨界值? 當(dāng)實(shí)際大小超過臨界值時(shí),會進(jìn)行擴(kuò)容threshold = 加載因子*容量finalfloatloadFactor;//加載因子transientintmodCount;//被修改的次數(shù)


其中l(wèi)oadFactor加載因子是表示Hsah表中元素的填滿的程度.

若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:沖突的機(jī)會加大了.鏈表長度會越來越長,查找效率降低。

反之,加載因子越小,填滿的元素越少,好處是:沖突的機(jī)會減小了,但:空間浪費(fèi)多了.表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴(kuò)容了)

沖突的機(jī)會越大,則查找的成本越高.

因此,必須在?"沖突的機(jī)會"與"空間利用率"之間尋找一種平衡與折衷.?這種平衡與折衷本質(zhì)上是數(shù)據(jù)結(jié)構(gòu)中有名的"時(shí)-空"矛盾的平衡與折衷.

如果機(jī)器內(nèi)存足夠,并且想要提高查詢速度的話可以將加載因子設(shè)置小一點(diǎn);相反如果機(jī)器內(nèi)存緊張,并且對查詢速度沒有什么要求的話可以將加載因子設(shè)置大一點(diǎn)。不過一般我們都不用去設(shè)置它,讓它取默認(rèn)值0.75就好了。


2、構(gòu)造方法

下面看看HashMap的幾個(gè)構(gòu)造方法:


publicHashMap(intinitialCapacity,float loadFactor) {

//確保數(shù)字合法if(initialCapacity <0)

thrownewIllegalArgumentException("Illegal initial capacity: "+? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initialCapacity);

if(initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

if(loadFactor <=0|| Float.isNaN(loadFactor))

thrownewIllegalArgumentException("Illegal load factor: "+? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? loadFactor);// Find a power of 2 >= initialCapacityintcapacity =1;//初始容量while(capacity < initialCapacity)//確保容量為2的n次冪,使capacity為大于initialCapacity的最小的2的n次冪capacity <<=1;this.loadFactor = loadFactor;threshold = (int)(capacity * loadFactor);table =new Entry[capacity];? ? ? ? init();? ? }publicHashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);? ? }public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);table =new Entry[DEFAULT_INITIAL_CAPACITY];? ? ? ? init();}


我們可以看到在構(gòu)造HashMap的時(shí)候如果我們指定了加載因子和初始容量的話就調(diào)用第一個(gè)構(gòu)造方法,否則的話就是用默認(rèn)的。默認(rèn)初始容量為16,默認(rèn)加載因子為0.75。我們可以看到上面代碼中13-15行,這段代碼的作用是確保容量為2的n次冪,使capacity為大于initialCapacity的最小的2的n次冪,至于為什么要把容量設(shè)置為2的n次冪,我們等下再看。


重點(diǎn)分析下HashMap中用的最多的兩個(gè)方法put和get

?3、存儲數(shù)據(jù)

  下面看看HashMap存儲數(shù)據(jù)的過程是怎樣的,首先看看HashMap的put方法:


public V put(K key, V value) {

? ? // 若“key為null”,則將該鍵值對添加到table[0]中。if(key ==null)

? ? ? ? ? ? return putForNullKey(value);

? ? // 若“key不為null”,則計(jì)算該key的哈希值,然后將其添加到該哈希值對應(yīng)的鏈表中。inthash = hash(key.hashCode());

? ? //搜索指定hash值在對應(yīng)table中的索引inti = indexFor(hash, table.length);

? ? // 循環(huán)遍歷Entry數(shù)組,若“該key”對應(yīng)的鍵值對已經(jīng)存在,則用新的value取代舊的value。然后退出!for(Entry e = table[i]; e !=null; e = e.next) {

? ? ? ? ? ? Object k;

? ? ? ? ? ? ? if(e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果key相同則覆蓋并返回舊值V oldValue = e.value;

? ? ? ? ? ? ? ? e.value = value;

? ? ? ? ? ? ? ? e.recordAccess(this);

? ? ? ? ? ? ? ? return oldValue;

? ? ? ? ? ? ? }

? ? ? ? }

? ? //修改次數(shù)+1modCount++;

? ? //將key-value添加到table[i]處? ? addEntry(hash, key, value, i);

? ? return null;

}


上面程序中用到了一個(gè)重要的內(nèi)部接口:Map.Entry,每個(gè)?Map.Entry?其實(shí)就是一個(gè)?key-value?對。從上面程序中可以看出:當(dāng)系統(tǒng)決定存儲?HashMap?中的?key-value?對時(shí),完全沒有考慮?Entry?中的?value,僅僅只是根據(jù)?key?來計(jì)算并決定每個(gè)?Entry?的存儲位置。這也說明了前面的結(jié)論:我們完全可以把?Map?集合中的?value?當(dāng)成?key?的附屬,當(dāng)系統(tǒng)決定了?key?的存儲位置之后,value?隨之保存在那里即可。

我們慢慢的來分析這個(gè)函數(shù),第2和3行的作用就是處理key值為null的情況,我們看看putForNullKey(value)方法:


private V putForNullKey(V value) {

for(Entry e = table[0]; e !=null; e = e.next) {

if(e.key ==null) {//如果有key為null的對象存在,則覆蓋掉V oldValue = e.value;

e.value = value;

e.recordAccess(this);

return oldValue;

? ? ? ? ? ? }

? ? ? ? }modCount++;addEntry(0,null, value,0);//如果鍵為null的話,則hash值為0returnnull;}


注意:如果key為null的話,hash值為0,對象存儲在數(shù)組中索引為0的位置。即table[0]

我們再回去看看put方法中第4行,它是通過key的hashCode值計(jì)算hash碼,下面是計(jì)算hash碼的函數(shù):


//計(jì)算hash值的方法 通過鍵的hashCode來計(jì)算staticinthash(int h) {// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>>20) ^ (h >>>12);returnh ^ (h >>>7) ^ (h >>>4);}


得到hash碼之后就會通過hash碼去計(jì)算出應(yīng)該存儲在數(shù)組中的索引,計(jì)算索引的函數(shù)如下:


staticintindexFor(inth,intlength) {//根據(jù)hash值和數(shù)組長度算出索引值returnh & (length-1);//這里不能隨便算取,用hash&(length-1)是有原因的,這樣可以確保算出來的索引是在數(shù)組大小范圍內(nèi),不會超出}


這個(gè)我們要重點(diǎn)說下,我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實(shí)現(xiàn)的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運(yùn)算,效率很低,HashMap中則通過h&(length-1)的方法來代替取模,同樣實(shí)現(xiàn)了均勻的散列,但效率要高很多,這也是HashMap對Hashtable的一個(gè)改進(jìn)。


? ??接下來,我們分析下為什么哈希表的容量一定要是2的整數(shù)次冪。首先,length為2的整數(shù)次冪的話,h&(length-1)就相當(dāng)于對length取模,這樣便保證了散列的均勻,同時(shí)也提升了效率;其次,length為2的整數(shù)次冪的話,為偶數(shù),這樣length-1為奇數(shù),奇數(shù)的最后一位是1,這樣便保證了h&(length-1)的最后一位可能為0,也可能為1(這取決于h的值),即與后的結(jié)果可能為偶數(shù),也可能為奇數(shù),這樣便可以保證散列的均勻性,而如果length為奇數(shù)的話,很明顯length-1為偶數(shù),它的最后一位是0,這樣h&(length-1)的最后一位肯定為0,即只能為偶數(shù),這樣任何hash值都只會被散列到數(shù)組的偶數(shù)下標(biāo)位置上,這便浪費(fèi)了近一半的空間,因此,length取2的整數(shù)次冪,是為了使不同hash值發(fā)生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。


  這看上去很簡單,其實(shí)比較有玄機(jī)的,我們舉個(gè)例子來說明:

假設(shè)數(shù)組長度分別為15和16,優(yōu)化后的hash碼分別為8和9,那么&運(yùn)算后的結(jié)果如下:?


h & (table.length-1)? ? ? ? ? ? ? ? ? ? hash? ? ? ? ? ? ? ? ? ? ? ? ? ? table.length-18& (15-1):1000&1110=10009& (15-1):1001&1110=1000-----------------------------------------------------------------------------------------------------------------------8& (16-1):1000&1111=10009& (16-1):1001&1111=1001





  從上面的例子中可以看出:

  當(dāng)它們和15-1(1110)“與”的時(shí)候,產(chǎn)生了相同的結(jié)果,也就是說它們會定位到數(shù)組中的同一個(gè)位置上去,這就產(chǎn)生了碰撞,8和9會被放到數(shù)組中的同一個(gè)位置上形成鏈表,那么查詢的時(shí)候就需要遍歷這個(gè)鏈表,得到8或者9,這樣就降低了查詢的效率。同時(shí),我們也可以發(fā)現(xiàn),當(dāng)數(shù)組長度為15的時(shí)候,hash值會與15-1(1110)進(jìn)行“與”,那么?最后一位永遠(yuǎn)是0,而0001,0011,0101,1001,1011,0111,1101這幾個(gè)位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長度小了很多,這意味著進(jìn)一步增加了碰撞的幾率,減慢了查詢的效率!

  而當(dāng)數(shù)組長度為16時(shí),即為2的n次方時(shí),2n-1得到的二進(jìn)制數(shù)的每個(gè)位上的值都為1,這使得在低位上&時(shí),得到的和原h(huán)ash的低位相同,加之hash(int?h)方法對key的hashCode的進(jìn)一步優(yōu)化,加入了高位計(jì)算,就使得只有相同的hash值的兩個(gè)值才會被放到數(shù)組中的同一個(gè)位置上形成鏈表。

  ?所以說,當(dāng)數(shù)組長度為2的n次冪的時(shí)候,不同的key算得得index相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時(shí)候就不用遍歷某個(gè)位置上的鏈表,這樣查詢效率也就較高了。

? ? 根據(jù)上面 put 方法的源代碼可以看出,當(dāng)程序試圖將一個(gè)key-value對放入HashMap中時(shí),程序首先根據(jù)該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:

  如果兩個(gè) Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。

  如果這兩個(gè) Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但key不會覆蓋。

  如果這兩個(gè) Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位于 Entry 鏈的頭部——

  具體說明繼續(xù)看 addEntry() 方法的說明。


voidaddEntry(inthash, K key, V value,int bucketIndex) {Entry e = table[bucketIndex];//如果要加入的位置有值,將該位置原先的值設(shè)置為新entry的next,也就是新entry鏈表的下一個(gè)節(jié)點(diǎn)table[bucketIndex] =newEntry<>(hash, key, value, e);if(size++ >= threshold)//如果大于臨界值就擴(kuò)容resize(2* table.length);//以2的倍數(shù)擴(kuò)容}


參數(shù)bucketIndex就是indexFor函數(shù)計(jì)算出來的索引值,第2行代碼是取得數(shù)組中索引為bucketIndex的Entry對象,第3行就是用hash、key、value構(gòu)建一個(gè)新的Entry對象放到索引為bucketIndex的位置,并且將該位置原先的對象設(shè)置為新對象的next構(gòu)成鏈表。

  第4行和第5行就是判斷put后size是否達(dá)到了臨界值threshold,如果達(dá)到了臨界值就要進(jìn)行擴(kuò)容,HashMap擴(kuò)容是擴(kuò)為原來的兩倍。


4、調(diào)整大小

resize()方法如下:

?重新調(diào)整HashMap的大小,newCapacity是調(diào)整后的單位

voidresize(int newCapacity) {

Entry[] oldTable = table;

intoldCapacity = oldTable.length;

if(oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

? ? ? ? }

Entry[] newTable =new Entry[newCapacity];transfer(newTable);//用來將原先table的元素全部移到newTable里面table = newTable;//再將newTable賦值給tablethreshold = (int)(newCapacity * loadFactor);//重新計(jì)算臨界值}


  新建了一個(gè)HashMap的底層數(shù)組,上面代碼中第10行為調(diào)用transfer方法,將HashMap的全部元素添加到新的HashMap中,并重新計(jì)算元素在新的數(shù)組中的索引位置


當(dāng)HashMap中的元素越來越多的時(shí)候,hash沖突的幾率也就越來越高,因?yàn)閿?shù)組的長度是固定的。所以為了提高查詢的效率,就要對HashMap的數(shù)組進(jìn)行擴(kuò)容,數(shù)組擴(kuò)容這個(gè)操作也會出現(xiàn)在ArrayList中,這是一個(gè)常用的操作,而在HashMap數(shù)組擴(kuò)容之后,最消耗性能的點(diǎn)就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計(jì)算其在新數(shù)組中的位置,并放進(jìn)去,這就是resize。


?? 那么HashMap什么時(shí)候進(jìn)行擴(kuò)容呢?當(dāng)HashMap中的元素個(gè)數(shù)超過數(shù)組大小*loadFactor時(shí),就會進(jìn)行數(shù)組擴(kuò)容,loadFactor的默認(rèn)值為0.75,這是一個(gè)折中的取值。也就是說,默認(rèn)情況下,數(shù)組大小為16,那么當(dāng)HashMap中元素個(gè)數(shù)超過16*0.75=12的時(shí)候,就把數(shù)組的大小擴(kuò)展為 2*16=32,即擴(kuò)大一倍,然后重新計(jì)算每個(gè)元素在數(shù)組中的位置,擴(kuò)容是需要進(jìn)行數(shù)組復(fù)制的,復(fù)制數(shù)組是非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知HashMap中元素的個(gè)數(shù),那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能。



5、數(shù)據(jù)讀取



publicVget(Object key) {? if(key ==null)? return getForNullKey();? inthash = hash(key.hashCode());? for(Entry e = table[indexFor(hash, table.length)];? e !=null;? e = e.next) {? ? ? ? ? Object k;? if(e.hash == hash && ((k = e.key) == key || key.equals(k)))? return e.value;? ? ? }? returnnull;? }

  有了上面存儲時(shí)的hash算法作為基礎(chǔ),理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中g(shù)et元素時(shí),首先計(jì)算key的hashCode,找到數(shù)組中對應(yīng)位置的某一元素,然后通過key的equals方法在對應(yīng)位置的鏈表中找到需要的元素。


歸納起來簡單地說,HashMap?在底層將?key-value?當(dāng)成一個(gè)整體進(jìn)行處理,這個(gè)整體就是一個(gè)?Entry?對象。HashMap?底層采用一個(gè)?Entry[]?數(shù)組來保存所有的?key-value?對,當(dāng)需要存儲一個(gè)?Entry?對象時(shí),會根據(jù)hash算法來決定其在數(shù)組中的存儲位置,在根據(jù)equals方法決定其在該數(shù)組位置上的鏈表中的存儲位置;當(dāng)需要取出一個(gè)Entry時(shí),也會根據(jù)hash算法找到其在數(shù)組中的存儲位置,再根據(jù)equals方法從該位置上的鏈表中取出該Entry。


6、HashMap的性能參數(shù):


?? HashMap 包含如下幾個(gè)構(gòu)造器:

?? HashMap():構(gòu)建一個(gè)初始容量為 16,負(fù)載因子為 0.75 的 HashMap。

?? HashMap(int initialCapacity):構(gòu)建一個(gè)初始容量為 initialCapacity,負(fù)載因子為 0.75 的 HashMap。

?? HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負(fù)載因子創(chuàng)建一個(gè) HashMap。

?? HashMap的基礎(chǔ)構(gòu)造器HashMap(int initialCapacity, float loadFactor)帶有兩個(gè)參數(shù),它們是初始容量initialCapacity和加載因子loadFactor。

?? initialCapacity:HashMap的最大容量,即為底層數(shù)組的長度。

?? loadFactor:負(fù)載因子loadFactor定義為:散列表的實(shí)際元素?cái)?shù)目(n)/ 散列表的容量(m)。

?? 負(fù)載因子衡量的是一個(gè)散列表的空間的使用程度,負(fù)載因子越大表示散列表的裝填程度越高,反之愈小。對于使用鏈表法的散列表來說,查找一個(gè)元素的平均時(shí)間是O(1+a),因此如果負(fù)載因子越大,對空間的利用更充分,然而后果是查找效率的降低;如果負(fù)載因子太小,那么散列表的數(shù)據(jù)將過于稀疏,對空間造成嚴(yán)重浪費(fèi)。

?? HashMap的實(shí)現(xiàn)中,通過threshold字段來判斷HashMap的最大容量:


threshold = (int)(capacity * loadFactor);

?? 結(jié)合負(fù)載因子的定義公式可知,threshold就是在此loadFactor和capacity對應(yīng)下允許的最大元素?cái)?shù)目,超過這個(gè)數(shù)目就重新resize,以降低實(shí)際的負(fù)載因子。默認(rèn)的的負(fù)載因子0.75是對空間和時(shí)間效率的一個(gè)平衡選擇。當(dāng)容量超出此最大容量時(shí), resize后的HashMap容量是容量的兩倍:


if(size++ >= threshold)? ? ?

? ? resize(2* table.length);

7、Fail-Fast機(jī)制:

?? 我們知道java.util.HashMap不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。

?? 這一策略在源碼中的實(shí)現(xiàn)是通過modCount域,modCount顧名思義就是修改次數(shù),對HashMap內(nèi)容的修改都將增加這個(gè)值,那么在迭代器初始化過程中會將這個(gè)值賦給迭代器的expectedModCount。

privateabstractclassHashIterator implements Iterator {

? ? ? ? Entry next;// next entry to returnintexpectedModCount;// For fast-failintindex;// current slotEntry current;// current entry? ? ? ? HashIterator() {

? ? ? ? ? ? expectedModCount = modCount;

? ? ? ? ? ? if(size >0) {// advance to first entryEntry[] t = table;

? ? ? ? ? ? ? ? while(index < t.length && (next = t[index++]) ==null)

? ? ? ? ? ? ? ? ? ? ;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? public final boolean hasNext() {

? ? ? ? ? ? returnnext !=null;

? ? ? ? }

? ? ? ? final Entry nextEntry() {

? ? ? ? ? ? if(modCount != expectedModCount)

? ? ? ? ? ? ? ? thrownew ConcurrentModificationException();

? ? ? ? ? ? Entry e = next;

? ? ? ? ? ? if(e ==null)

? ? ? ? ? ? ? ? thrownew NoSuchElementException();

? ? ? ? ? ? if((next = e.next) ==null) {

? ? ? ? ? ? ? ? Entry[] t = table;

? ? ? ? ? ? ? ? while(index < t.length && (next = t[index++]) ==null)

? ? ? ? ? ? ? ? ? ? ;

? ? ? ? ? ? }

? ? ? ? current = e;

? ? ? ? ? ? return e;

? ? ? ? }

? ? ? ? publicvoid remove() {

? ? ? ? ? ? if(current ==null)

? ? ? ? ? ? ? ? thrownew IllegalStateException();

? ? ? ? ? ? if(modCount != expectedModCount)

? ? ? ? ? ? ? ? thrownew ConcurrentModificationException();

? ? ? ? ? ? Object k = current.key;

? ? ? ? ? ? current =null;

? ? ? ? ? ? HashMap.this.removeEntryForKey(k);

? ? ? ? ? ? expectedModCount = modCount;

? ? ? ? }

? ? }


在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經(jīng)有其他線程修改了Map:

?? 注意到modCount聲明為volatile,保證線程之間修改的可見性。

final Entry nextEntry() {? ? ? if(modCount != expectedModCount)? ? ? thrownewConcurrentModificationException();

在HashMap的API中指出:

?? 由所有HashMap類的“collection 視圖方法”所返回的迭代器都是快速失敗的:在迭代器創(chuàng)建之后,如果從結(jié)構(gòu)上對映射進(jìn)行修改,除非通過迭代器本身的 remove 方法,其他任何時(shí)間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對并發(fā)的修改,迭代器很快就會完全失敗,而不冒在將來不確定的時(shí)間發(fā)生任意不確定行為的風(fēng)險(xiǎn)。

?? 注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的并發(fā)修改時(shí),不可能作出任何堅(jiān)決的保證??焖偈〉鞅M最大努力拋出 ConcurrentModificationException。因此,編寫依賴于此異常的程序的做法是錯(cuò)誤的,正確做法是:迭代器的快速失敗行為應(yīng)該僅用于檢測程序錯(cuò)誤


參考鏈接:

深入Java集合學(xué)習(xí)系列:HashMap的實(shí)現(xiàn)原理

分類:?Java集合

標(biāo)簽:?Java,?集合,?HashMap

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,451評論 0 16
  • 實(shí)際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,561評論 1 37
  • 5.1、對于HashMap需要掌握以下幾點(diǎn) Map的創(chuàng)建:HashMap() 往Map中添加鍵值對:即put(Ob...
    rochuan閱讀 866評論 0 0
  • 2014年的8月,我開始進(jìn)入到健身房進(jìn)行健身。說到健身,他是我很早以來的一個(gè)愿望。從小我的身體抵抗能力就比較差,容...
    張立夏物華天寶人杰地靈閱讀 252評論 0 0
  • “我想成為海里的浪,風(fēng)中的云,但我還只是小小的我,有一天我要跳出自己的身軀,我要搖晃天空,像一把小提琴……”...
    鉛字筆小姐閱讀 333評論 0 0

友情鏈接更多精彩內(nèi)容