1.??? HashMap 概述:
??? HashMap 是基于哈希表的 Map 接口的非同步實(shí)現(xiàn)。此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。
?
2.??? HashMap 的數(shù)據(jù)結(jié)構(gòu):
??? 在 java 編程語言中,最基本的結(jié)構(gòu)就是兩種,一個(gè)是數(shù)組,另外一個(gè)是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個(gè)基本結(jié)構(gòu)來構(gòu)造的, HashMap 也不例外。 HashMap 實(shí)際上是一個(gè)“鏈表散列”的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體。
?? 從上圖中可以看出, HashMap 底層就是一個(gè)數(shù)組結(jié)構(gòu),數(shù)組中的每一項(xiàng)又是一個(gè)鏈表。當(dāng)新建一個(gè) HashMap 的時(shí)候,就會(huì)初始化一個(gè)數(shù)組。
?? 源碼如下:
- /** ?
- ?*?The?table,?resized?as?necessary.?Length?MUST?Always?be?a?power?of?two. ?
- ?*/ ??
- transient ?Entry[]?table;??
- ??
- static ? class ?Entry<K,V>? implements ?Map.Entry<K,V>?{??
- ???? final ?K?key;??
- ????V?value;??
- ????Entry<K,V>?next;??
- ???? final ? int ?hash;??
- ????……??
- }??
?
?? 可以看出, Entry 就是數(shù)組中的元素,每個(gè) ? Map.Entry ? 其實(shí)就是一個(gè) key-value 對(duì),它持有一個(gè)指向下一個(gè)元素的引用,這就構(gòu)成了鏈表。
?
3.??? HashMap 的存取實(shí)現(xiàn):
?? 1) 存儲(chǔ):
?
- public ?V?put(K?key,?V?value)?{??
- ???? //?HashMap允許存放null鍵和null值。 ??
- ???? //?當(dāng)key為null時(shí),調(diào)用putForNullKey方法,將value放置在數(shù)組第一個(gè)位置。 ??
- ???? if ?(key?==? null )??
- ???????? return ?putForNullKey(value);??
- ???? //?根據(jù)key的keyCode重新計(jì)算hash值。 ??
- ???? int ?hash?=?hash(key.hashCode());??
- ???? //?搜索指定hash值在對(duì)應(yīng)table中的索引。 ??
- ???? int ?i?=?indexFor(hash,?table.length);??
- ???? //?如果?i?索引處的?Entry?不為?null,通過循環(huán)不斷遍歷?e?元素的下一個(gè)元素。 ??
- ???? for ?(Entry<K,V>?e?=?table[i];?e?!=? null ;?e?=?e.next)?{??
- ????????Object?k;??
- ???????? if ?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?key.equals(k)))?{??
- ????????????V?oldValue?=?e.value;??
- ????????????e.value?=?value;??
- ????????????e.recordAccess( this );??
- ???????????? return ?oldValue;??
- ????????}??
- ????}??
- ???? //?如果i索引處的Entry為null,表明此處還沒有Entry。 ??
- ????modCount++;??
- ???? //?將key、value添加到i索引處。 ??
- ????addEntry(hash,?key,?value,?i);??
- ???? return ? null ;??
- }??
?
?
?? 從上面的源代碼中可以看出:當(dāng)我們往 HashMap 中 put 元素的時(shí)候,先根據(jù) key 的 hashCode 重新計(jì)算 hash 值,根據(jù) hash 值得到這個(gè)元素在數(shù)組中的位置(即下標(biāo)),如果數(shù)組該位置上已經(jīng)存放有其他元素了,那么在這個(gè)位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數(shù)組該位置上沒有元素,就直接將該元素放到此數(shù)組中的該位置上。
?? ? addEntry(hash, key, value, i) 方法根據(jù)計(jì)算出的 hash 值,將 key-value 對(duì)放在數(shù)組 table 的 i 索引處。 addEntry ? 是 ? HashMap ? 提供的一個(gè)包訪問權(quán)限的方法,代碼如下:
?
- void ?addEntry( int ?hash,?K?key,?V?value,? int ?bucketIndex)?{??
- ???? //?獲取指定?bucketIndex?索引處的?Entry? ??
- ????Entry<K,V>?e?=?table[bucketIndex];??
- ???? //?將新創(chuàng)建的?Entry?放入?bucketIndex?索引處,并讓新的?Entry?指向原來的?Entry ??
- ????table[bucketIndex]?=? new ?Entry<K,V>(hash,?key,?value,?e);??
- ???? //?如果?Map?中的?key-value?對(duì)的數(shù)量超過了極限 ??
- ???? if ?(size++?>=?threshold)??
- ???? //?把?table?對(duì)象的長(zhǎng)度擴(kuò)充到原來的2倍。 ??
- ????????resize( 2 ?*?table.length);??
- }??
?
?
?? 當(dāng)系統(tǒng)決定存儲(chǔ) HashMap 中的 key-value 對(duì)時(shí),完全沒有考慮 Entry 中的 value ,僅僅只是根據(jù) key 來計(jì)算并決定每個(gè) Entry 的存儲(chǔ)位置。我們完全可以把 ? Map ? 集合中的 ? value ? 當(dāng)成 ? key ? 的附屬,當(dāng)系統(tǒng)決定了 ? key ? 的存儲(chǔ)位置之后, value ? 隨之保存在那里即可。
?? ? hash(int h) 方法根據(jù) key 的 hashCode 重新計(jì)算一次散列。此算法加入了高位計(jì)算,防止低位不變,高位變化時(shí),造成的 hash 沖突。
?
- static ? int ?hash( int ?h)?{??
- ????h?^=?(h?>>>? 20 )?^?(h?>>>? 12 );??
- ???? return ?h?^?(h?>>>? 7 )?^?(h?>>>? 4 );??
- }??
?
?
?? 我們可以看到在 HashMap 中要找到某個(gè)元素,需要根據(jù) key 的 hash 值來求得對(duì)應(yīng)數(shù)組中的位置。如何計(jì)算這個(gè)位置就是 hash 算法。前面說過 HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,所以我們當(dāng)然希望這個(gè) HashMap 里面的 ? 元素位置盡量的分布均勻些,盡量使得每個(gè)位置上的元素?cái)?shù)量只有一個(gè),那么當(dāng)我們用 hash 算法求得這個(gè)位置的時(shí)候,馬上就可以知道對(duì)應(yīng)位置的元素就是我們要的,而不用再去遍歷鏈表,這樣就大大優(yōu)化了查詢的效率。
?? ? 對(duì)于任意給定的對(duì)象,只要它的 ? hashCode() ? 返回值相同,那么程序調(diào)用 ? hash(int h) ? 方法所計(jì)算得到的 ? hash ? 碼值總是相同的。我們首先想到的就是把 hash 值對(duì)數(shù)組長(zhǎng)度取模運(yùn)算,這樣一來,元素的分布相對(duì)來說是比較均勻的。但是, “ 模 ” 運(yùn)算的消耗還是比較大的,在 HashMap 中是這樣做的:調(diào)用 ? indexFor(int h, int length) ? 方法來計(jì)算該對(duì)象應(yīng)該保存在 ? table ? 數(shù)組的哪個(gè)索引處。 indexFor(int h, int length) ? 方法的代碼如下:
?
- static ? int ?indexFor( int ?h,? int ?length)?{??
- ???? return ?h?&?(length- 1 );??
- }??
?
?
?? 這個(gè)方法非常巧妙,它通過 ? h & (table.length -1) ? 來得到該對(duì)象的保存位,而 HashMap 底層數(shù)組的長(zhǎng)度總是 ? 2 ? 的 ? n ? 次方,這是 HashMap 在速度上的優(yōu)化。在 ? HashMap ? 構(gòu)造器中有如下代碼:
- int ?capacity?=? 1 ;??
- ???? while ?(capacity?<?initialCapacity)??
- ????????capacity?<<=? 1 ;??
?? 這段代碼保證初始化時(shí) HashMap 的容量總是 2 的 n 次方,即底層數(shù)組的長(zhǎng)度總是為 2 的 n 次方。
當(dāng) length 總是 ? 2 ? 的 n 次方時(shí), h& (length-1) 運(yùn)算等價(jià)于對(duì) length 取模,也就是 h%length ,但是 & 比 % 具有更高的效率。
?? ? 這看上去很簡(jiǎn)單,其實(shí)比較有玄機(jī)的,我們舉個(gè)例子來說明:
?? ? 假設(shè)數(shù)組長(zhǎng)度分別為 15 和 16 ,優(yōu)化后的 hash 碼分別為 8 和 9 ,那么 & 運(yùn)算后的結(jié)果如下:
?????? ? h & (table.length-1) ??? ? ????????? ? ????? ? hash ???? ? ?????????????????????? ? table.length-1
?????? ? 8 & (15-1) : ???????????????????????????????? ? 0100 ? ? ???? ? ??????????? ? &? ? ???????????? 1110? ????????????????? ? = ? ????????????? ? 0100
? ???? ? 9 & (15-1) : ???????????????????????????????? ? 0101 ?????????????????? ? & ?????????????? 1110 ????????????????? ? = ??????????????? ? 0100
?????? ? -----------------------------------------------------------------------------------------------------------------------
?????? 8 & (16-1) : ???????????????????????????????? ? 0100 ?????????????????? ? & ? ???????????? ? 1111 ?????????????????? ? = ??????????????? ? 0100
?????? 9 & (16-1) : ???????????????????????????????? ? 0101 ?????????????????? ? & ? ???????????? ? 1111 ?????????????????? ? = ??????????????? ? 0101
??
?? ? 從上面的例子中可以看出:當(dāng)它們和 15-1 ( 1110 ) “ 與 ” 的時(shí)候,產(chǎn)生了相同的結(jié)果,也就是說它們會(huì)定位到數(shù)組中的同一個(gè)位置上去,這就產(chǎn)生了碰撞, 8 和 9 會(huì)被放到數(shù)組中的同一個(gè)位置上形成鏈表,那么查詢的時(shí)候就需要遍歷這個(gè)鏈 ? 表,得到 8 或者 9 ,這樣就降低了查詢的效率。同時(shí),我們也可以發(fā)現(xiàn),當(dāng)數(shù)組長(zhǎng)度為 15 的時(shí)候, hash 值會(huì)與 15-1 ( 1110 )進(jìn)行 “ 與 ” ,那么 ? 最后一位永遠(yuǎn)是 0 ,而 0001 , 0011 , 0101 , 1001 , 1011 , 0111 , 1101 這幾個(gè)位置永遠(yuǎn)都不能存放元素了,空間浪費(fèi)相當(dāng)大,更糟的是這種情況中,數(shù)組可以使用的位置比數(shù)組長(zhǎng)度小了很多,這意味著進(jìn)一步增加了碰撞的幾率,減慢了查詢的效率!而當(dāng)數(shù)組長(zhǎng)度為 16 時(shí),即為 2 的 n 次方時(shí), 2 n -1 得到的二進(jìn)制數(shù)的每個(gè)位上的值都為 1 ,這使得在低位上 & 時(shí),得到的和原 hash 的低位相同,加之 hash(int h) 方法對(duì) key 的 hashCode 的進(jìn)一步優(yōu)化,加入了高位計(jì)算,就使得只有相同的 hash 值的兩個(gè)值才會(huì)被放到數(shù)組中的同一個(gè)位置上形成鏈表。
???
?? ? 所以說,當(dāng)數(shù)組長(zhǎng)度為 2 的 n 次冪的時(shí)候,不同的 key 算得得 index 相同的幾率較小,那么數(shù)據(jù)在數(shù)組上分布就比較均勻,也就是說碰撞的幾率小,相對(duì)的,查詢的時(shí)候就不用遍歷某個(gè)位置上的鏈表,這樣查詢效率也就較高了。
?? ? 根據(jù)上面 ? put ? 方法的源代碼可以看出,當(dāng)程序試圖將一個(gè) key-value 對(duì)放入 HashMap 中時(shí),程序首先根據(jù)該 ? key ? 的 ? hashCode() ? 返回值決定該 ? Entry ? 的存儲(chǔ)位置:如果兩個(gè) ? Entry ? 的 ? key ? 的 ? hashCode() ? 返回值相同,那它們的存儲(chǔ)位置相同。如果這兩個(gè) ? Entry ? 的 ? key ? 通過 equals ? 比較返回 ? true ,新添加 ? Entry ? 的 ? value ? 將覆蓋集合中原有 ? Entry ? 的 ? value ,但 key 不會(huì)覆蓋。如果這兩個(gè) ? Entry ? 的 ? key ? 通過 ? equals 比較返回 ? false ,新添加的 ? Entry ? 將與集合中原有 ? Entry ? 形成 ? Entry ? 鏈,而且新添加的 ? Entry ? 位于 ? Entry ? 鏈的頭部 —— 具體說明繼續(xù)看 addEntry() ? 方法的說明。
?? 2) ? 讀取:
- public ?V?get(Object?key)?{??
- ???? if ?(key?==? null )??
- ???????? return ?getForNullKey();??
- ???? int ?hash?=?hash(key.hashCode());??
- ???? for ?(Entry<K,V>?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;??
- ????}??
- ???? return ? null ;??
- }??
?
?? 有了上面存儲(chǔ)時(shí)的 hash 算法作為基礎(chǔ),理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從 HashMap 中 get 元素時(shí),首先計(jì)算 key 的 hashCode ,找到數(shù)組中對(duì)應(yīng)位置的某一元素,然后通過 key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的元素。
??
?? 3) ? 歸納起來簡(jiǎn)單地說, HashMap ? 在底層將 ? key-value ? 當(dāng)成一個(gè)整體進(jìn)行處理,這個(gè)整體就是一個(gè) ? Entry ? 對(duì)象。 HashMap ? 底層采用一個(gè) ? Entry[] ? 數(shù)組來保存所有的 ? key-value ? 對(duì),當(dāng)需要存儲(chǔ)一個(gè) ? Entry ? 對(duì)象時(shí),會(huì)根據(jù) hash 算法來決定其在數(shù)組中的存儲(chǔ)位置,在根據(jù) equals 方法決定其在該數(shù)組位置上的鏈表中的存儲(chǔ)位置;當(dāng)需要取出一個(gè) Entry 時(shí),也會(huì)根據(jù) hash 算法找到其在數(shù)組中的存儲(chǔ)位置,再根據(jù) equals 方法從該位置上的鏈表中取出該 Entry 。
?
4.??? HashMap 的 resize ( rehash ):
?? ? 當(dāng) HashMap 中的元素越來越多的時(shí)候, hash 沖突的幾率也就越來越高,因?yàn)閿?shù)組的長(zhǎng)度是固定的。所以為了提高查詢的效率,就要對(duì) HashMap 的數(shù)組進(jìn)行擴(kuò)容,數(shù)組擴(kuò)容這個(gè)操作也會(huì)出現(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í),就會(huì)進(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ù)組中的位置,而這是一個(gè)非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知 HashMap 中元素的個(gè)數(shù),那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高 HashMap 的性能。
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
