溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

什么是HashMap

發(fā)布時(shí)間:2021-09-17 09:26:24 來源:億速云 閱讀:114 作者:柒染 欄目:web開發(fā)

什么是HashMap,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。

什么是HashMap

HashMap是一個(gè)非常重要的集合,日常使用也非常的頻繁,同時(shí)也是面試重點(diǎn)。本文并不打算講解基礎(chǔ)的使用api,而是深入HashMap的底層,講解關(guān)于HashMap的重點(diǎn)知識(shí)。需要讀者對(duì)散列表和HashMap有一定的認(rèn)識(shí)。

HashMap本質(zhì)上是一個(gè)散列表,那么就離不開散列表的三大問題:散列函數(shù)、哈希沖突、擴(kuò)容方案;同時(shí)作為一個(gè)數(shù)據(jù)結(jié)構(gòu),必須考慮多線程并發(fā)訪問的問題,也就是線程安全。這四大重點(diǎn)則為學(xué)習(xí)HashMap的重點(diǎn),也是HashMap設(shè)計(jì)的重點(diǎn)。

HashMap屬于Map集合體系的一部分,同時(shí)繼承了Serializable接口可以被序列化,繼承了Cloneable接口可以被復(fù)制。他的的繼承結(jié)構(gòu)如下:

什么是HashMap

img

HashMap并不是全能的,對(duì)于一些特殊的情景下的需求官方拓展了一些其他的類來滿足,如線程安全的ConcurrentHashMap、記錄插入順序的LinkHashMap、給key排序的TreeMap等。

文章內(nèi)容主要講解四大重點(diǎn):散列函數(shù)、哈希沖突、擴(kuò)容方案、線程安全,再補(bǔ)充關(guān)鍵的源碼分析和相關(guān)的問題。

本文所有內(nèi)容如若未特殊說明,均為JDK1.8版本。

哈希函數(shù)

哈希函數(shù)的目標(biāo)是計(jì)算key在數(shù)組中的下標(biāo)。判斷一個(gè)哈希函數(shù)的標(biāo)準(zhǔn)是:散列是否均勻、計(jì)算是否簡(jiǎn)單。

HashMap哈希函數(shù)的步驟:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 對(duì)key對(duì)象的hashcode進(jìn)行擾動(dòng)

  3. 通過取模求得數(shù)組下標(biāo)

擾動(dòng)是為了讓hashcode的隨機(jī)性更高,第二步取模就不會(huì)讓所以的key都聚集在一起,提高散列均勻度。擾動(dòng)可以看到hash()方法:

static final int hash(Object key) {     int h;     // 獲取到key的hashcode,在高低位異或運(yùn)算     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

也就是低16位是和高16位進(jìn)行異或,高16位保持不變。一般的數(shù)組長(zhǎng)度都會(huì)比較短,取模運(yùn)算中只有低位參與散列;高位與低位進(jìn)行異或,讓高位也得以參與散列運(yùn)算,使得散列更加均勻。具體運(yùn)算如下圖(圖中為了方便采用8位進(jìn)行演示,32位同理):

什么是HashMap

img

對(duì)hashcode擾動(dòng)之后需要對(duì)結(jié)果進(jìn)行取模。HashMap在jdk1.8并不是簡(jiǎn)單使用%進(jìn)行取模,而是采用了另外一種更加高性能的方法。HashMap控制數(shù)組長(zhǎng)度為2的整數(shù)次冪,好處是對(duì)hashcode進(jìn)行求余運(yùn)算和讓hashcode與數(shù)組長(zhǎng)度-1進(jìn)行位與運(yùn)算是相同的效果。如下圖:

什么是HashMap

img

但位與運(yùn)算的效率卻比求余高得多,從而提升了性能。在擴(kuò)容運(yùn)算中也利用到了此特性,后面會(huì)講。取模運(yùn)算的源碼看到putVal()方法,該方法在put()方法中被調(diào)用:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                boolean evict) {     ...     // 與數(shù)組長(zhǎng)度-1進(jìn)行位與運(yùn)算,得到下標(biāo)     if ((p = tab[i = (n - 1) & hash]) == null)         ... }

完整的hash計(jì)算過程可以參考下圖:

什么是HashMap

img

上面我們提到HashMap的數(shù)組長(zhǎng)度為2的整數(shù)次冪,那么HashMap是如何控制數(shù)組的長(zhǎng)度為2的整數(shù)次冪的?修改數(shù)組長(zhǎng)度有兩種情況:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 初始化時(shí)指定的長(zhǎng)度

  3. 擴(kuò)容時(shí)的長(zhǎng)度增量

先看第一種情況。默認(rèn)情況下,如未在HashMap構(gòu)造器中指定長(zhǎng)度,則初始長(zhǎng)度為16。16是一個(gè)較為合適的經(jīng)驗(yàn)值,他是2的整數(shù)次冪,同時(shí)太小會(huì)頻繁觸發(fā)擴(kuò)容、太大會(huì)浪費(fèi)空間。如果指定一個(gè)非2的整數(shù)次冪,會(huì)自動(dòng)轉(zhuǎn)化成大于該指定數(shù)的最小2的整數(shù)次冪。如指定6則轉(zhuǎn)化為8,指定11則轉(zhuǎn)化為16。結(jié)合源碼來分析,當(dāng)我們初始化指定一個(gè)非2的整數(shù)次冪長(zhǎng)度時(shí),HashMap會(huì)調(diào)用tableSizeFor()方法:

public HashMap(int initialCapacity, float loadFactor) {     ...     this.loadFactor = loadFactor;     // 這里調(diào)用了tableSizeFor方法     this.threshold = tableSizeFor(initialCapacity); }  static final int tableSizeFor(int cap) {     // 注意這里必須減一     int n = cap - 1;     n |= n >>> 1;     n |= n >>> 2;     n |= n >>> 4;     n |= n >>> 8;     n |= n >>> 16;     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

tableSizeFor()方法的看起來很復(fù)雜,作用是使得最高位1后續(xù)的所有位都變?yōu)?,最后再+1則得到剛好大于initialCapacity的最小2的整數(shù)次冪數(shù)。如下圖(這里使用了8位進(jìn)行模擬,32位也是同理):

什么是HashMap

img

那為什么必須要對(duì)cap進(jìn)行-1之后再進(jìn)行運(yùn)算呢?如果指定的數(shù)剛好是2的整數(shù)次冪,如果沒有-1結(jié)果會(huì)變成比他大兩倍的數(shù),如下:

00100 --高位1之后全變1--> 00111 --加1---> 01000

第二種改變數(shù)組長(zhǎng)度的情況是擴(kuò)容。HashMap每次擴(kuò)容的大小都是原來的兩倍,控制了數(shù)組大小一定是2的整數(shù)次冪,相關(guān)源碼如下:

final Node<K,V>[] resize() {     ...     if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                  oldCap >= DEFAULT_INITIAL_CAPACITY)             // 設(shè)置為原來的兩倍             newThr = oldThr << 1;     ... }

小結(jié)

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. HashMap通過高16位與低16位進(jìn)行異或運(yùn)算來讓高位參與散列,提高散列效果;

  3. HashMap控制數(shù)組的長(zhǎng)度為2的整數(shù)次冪來簡(jiǎn)化取模運(yùn)算,提高性能;

  4. HashMap通過控制初始化的數(shù)組長(zhǎng)度為2的整數(shù)次冪、擴(kuò)容為原來的2倍來控制數(shù)組長(zhǎng)度一定為2的整數(shù)次冪。

哈希沖突解決方案

再優(yōu)秀的hash算法永遠(yuǎn)無法避免出現(xiàn)hash沖突。hash沖突指的是兩個(gè)不同的key經(jīng)過hash計(jì)算之后得到的數(shù)組下標(biāo)是相同的。解決hash沖突的方式很多,如開放定址法、再哈希法、公共溢出表法、鏈地址法。HashMap采用的是鏈地址法,jdk1.8之后還增加了紅黑樹的優(yōu)化,如下圖:

什么是HashMap

img

出現(xiàn)沖突后會(huì)在當(dāng)前節(jié)點(diǎn)形成鏈表,而當(dāng)鏈表過長(zhǎng)之后,會(huì)自動(dòng)轉(zhuǎn)化成紅黑樹提高查找效率。紅黑樹是一個(gè)查找效率很高的數(shù)據(jù)結(jié)構(gòu),時(shí)間復(fù)雜度為O(logN),但紅黑樹只有在數(shù)據(jù)量較大時(shí)才能發(fā)揮它的優(yōu)勢(shì)。關(guān)于紅黑樹的轉(zhuǎn)化,HashMap做了以下限制。

  • 當(dāng)鏈表的長(zhǎng)度>=8且數(shù)組長(zhǎng)度>=64時(shí),會(huì)把鏈表轉(zhuǎn)化成紅黑樹。

  • 當(dāng)鏈表長(zhǎng)度>=8,但數(shù)組長(zhǎng)度<64時(shí),會(huì)優(yōu)先進(jìn)行擴(kuò)容,而不是轉(zhuǎn)化成紅黑樹。

  • 當(dāng)紅黑樹節(jié)點(diǎn)數(shù)<=6,自動(dòng)轉(zhuǎn)化成鏈表。

那就有了以下問題:

為什么需要數(shù)組長(zhǎng)度到64才會(huì)轉(zhuǎn)化紅黑樹?

當(dāng)數(shù)組長(zhǎng)度較短時(shí),如16,鏈表長(zhǎng)度達(dá)到8已經(jīng)是占用了最大限度的50%,意味著負(fù)載已經(jīng)快要達(dá)到上限,此時(shí)如果轉(zhuǎn)化成紅黑樹,之后的擴(kuò)容又會(huì)再一次把紅黑樹拆分平均到新的數(shù)組中,這樣非但沒有帶來性能的好處,反而會(huì)降低性能。所以在數(shù)組長(zhǎng)度低于64時(shí),優(yōu)先進(jìn)行擴(kuò)容。

為什么要大于等于8轉(zhuǎn)化為紅黑樹,而不是7或9?

樹節(jié)點(diǎn)的比普通節(jié)點(diǎn)更大,在鏈表較短時(shí)紅黑樹并未能明顯體現(xiàn)性能優(yōu)勢(shì),反而會(huì)浪費(fèi)空間,在鏈表較短是采用鏈表而不是紅黑樹。在理論數(shù)學(xué)計(jì)算中(裝載因子=0.75),鏈表的長(zhǎng)度到達(dá)8的概率是百萬分之一;把7作為分水嶺,大于7轉(zhuǎn)化為紅黑樹,小于7轉(zhuǎn)化為鏈表。紅黑樹的出現(xiàn)是為了在某些極端的情況下,抗住大量的hash沖突,正常情況下使用鏈表是更加合適的。

注意,紅黑樹在jdk1.8之后出現(xiàn)的,jdk1.7采用的是數(shù)組+鏈表模式。

小結(jié)

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. HashMap采用鏈地址法,當(dāng)發(fā)生沖突時(shí)會(huì)轉(zhuǎn)化為鏈表,當(dāng)鏈表過長(zhǎng)會(huì)轉(zhuǎn)化為紅黑樹提高效率。

  3. HashMap對(duì)紅黑樹進(jìn)行了限制,讓紅黑樹只有在極少數(shù)極端情況下進(jìn)行抗壓。

擴(kuò)容方案

當(dāng)HashMap中的數(shù)據(jù)越來越多,那么發(fā)生hash沖突的概率也就會(huì)越來越高,通過數(shù)組擴(kuò)容可以利用空間換時(shí)間,保持查找效率在常數(shù)時(shí)間復(fù)雜度。那什么時(shí)候進(jìn)行擴(kuò)容?由HashMap的一個(gè)關(guān)鍵參數(shù)控制:裝載因子。

裝載因子=HashMap中節(jié)點(diǎn)數(shù)/數(shù)組長(zhǎng)度,他是一個(gè)比例值。當(dāng)HashMap中節(jié)點(diǎn)數(shù)到達(dá)裝載因子這個(gè)比例時(shí),就會(huì)觸發(fā)擴(kuò)容;也就是說,裝載因子控制了當(dāng)前數(shù)組能夠承載的節(jié)點(diǎn)數(shù)的閾值。如數(shù)組長(zhǎng)度是16,裝載因子是0.75,那么可容納的節(jié)點(diǎn)數(shù)是16*0.75=12。裝載因子的數(shù)值大小需要仔細(xì)權(quán)衡。裝載因子越大,數(shù)組利用率越高,同時(shí)發(fā)生哈希沖突的概率也就越高;裝載因子越小,數(shù)組利用率降低,但發(fā)生哈希沖突的概率也降低了。所以裝載因子的大小需要權(quán)衡空間與時(shí)間之間的關(guān)系。在理論計(jì)算中,0.75是一個(gè)比較合適的數(shù)值,大于0.75哈希沖突的概率呈指數(shù)級(jí)別上升,而小于0.75沖突減少并不明顯。HashMap中的裝載因子的默認(rèn)大小是0.75,沒有特殊要求的情況下,不建議修改他的值。

那么在到達(dá)閾值之后,HashMap是如何進(jìn)行擴(kuò)容的呢?HashMap會(huì)把數(shù)組長(zhǎng)度擴(kuò)展為原來的兩倍,再把舊數(shù)組的數(shù)據(jù)遷移到新的數(shù)組,而HashMap針對(duì)遷移做了優(yōu)化:使用HashMap數(shù)組長(zhǎng)度是2的整數(shù)次冪的特點(diǎn),以一種更高效率的方式完成數(shù)據(jù)遷移。

JDK1.7之前的數(shù)據(jù)遷移比較簡(jiǎn)單,就是遍歷所有的節(jié)點(diǎn),把所有的節(jié)點(diǎn)依次通過hash函數(shù)計(jì)算新的下標(biāo),再插入到新數(shù)組的鏈表中。這樣會(huì)有兩個(gè)缺點(diǎn):1、每個(gè)節(jié)點(diǎn)都需要進(jìn)行一次求余計(jì)算;2、插入到新的數(shù)組時(shí)候采用的是頭插入法,在多線程環(huán)境下會(huì)形成鏈表環(huán)。jdk1.8之后進(jìn)行了優(yōu)化,原因在于他控制數(shù)組的長(zhǎng)度始終是2的整數(shù)次冪,每次擴(kuò)展數(shù)組都是原來的2倍,帶來的好處是key在新的數(shù)組的hash結(jié)果只有兩種:在原來的位置,或者在原來位置+原數(shù)組長(zhǎng)度。具體為什么我們可以看下圖:

什么是HashMap

img

從圖中我們可以看到,在新數(shù)組中的hash結(jié)果,僅僅取決于高一位的數(shù)值。如果高一位是0,那么計(jì)算結(jié)果就是在原位置,而如果是1,則加上原數(shù)組的長(zhǎng)度即可。這樣我們只需要判斷一個(gè)節(jié)點(diǎn)的高一位是1  or  0就可以得到他在新數(shù)組的位置,而不需要重復(fù)hash計(jì)算。HashMap把每個(gè)鏈表拆分成兩個(gè)鏈表,對(duì)應(yīng)原位置或原位置+原數(shù)組長(zhǎng)度,再分別插入到新的數(shù)組中,保留原來的節(jié)點(diǎn)順序,如下:

什么是HashMap

img

小結(jié)

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 裝載因子決定了HashMap擴(kuò)容的閾值,需要權(quán)衡時(shí)間與空間,一般情況下保持0.75不作改動(dòng);

  3. HashMap擴(kuò)容機(jī)制結(jié)合了數(shù)組長(zhǎng)度為2的整數(shù)次冪的特點(diǎn),以一種更高的效率完成數(shù)據(jù)遷移,同時(shí)避免頭插法造成鏈表環(huán)。

線程安全

HashMap作為一個(gè)集合,主要功能則為CRUD,也就是增刪查改數(shù)據(jù),那么就肯定涉及到多線程并發(fā)訪問數(shù)據(jù)的情況。并發(fā)產(chǎn)生的問題,需要我們特別關(guān)注。

HashMap并不是線程安全的,在多線程的情況下無法保證數(shù)據(jù)的一致性。舉個(gè)例子:HashMap下標(biāo)2的位置為null,線程A需要將節(jié)點(diǎn)X插入下標(biāo)2的位置,在判斷是否為null之后,線程被掛起;此時(shí)線程B把新的節(jié)點(diǎn)Y插入到下標(biāo)2的位置;恢復(fù)線程A,節(jié)點(diǎn)X會(huì)直接插入到下標(biāo)2,覆蓋節(jié)點(diǎn)Y,導(dǎo)致數(shù)據(jù)丟失,如下圖:

jdk1.7及以前擴(kuò)容時(shí)采用的是頭插法,這種方式插入速度快,但在多線程環(huán)境下會(huì)造成鏈表環(huán),而鏈表環(huán)會(huì)在下一次插入時(shí)找不到鏈表尾而發(fā)生死循環(huán)。

那如果結(jié)果數(shù)據(jù)一致性問題呢?解決這個(gè)問題有三個(gè)方案:

  • 采用Hashtable

  • 調(diào)用Collections.synchronizeMap()方法來讓HashMap具有多線程能力

  • 采用ConcurrentHashMap

前兩個(gè)方案的思路是相似的,均是每個(gè)方法中,對(duì)整個(gè)對(duì)象進(jìn)行上鎖。Hashtable是老一代的集合框架,很多的設(shè)計(jì)均以及落后,他在每一個(gè)方法中均加上了synchronize關(guān)鍵字保證線程安全。

// Hashtable public synchronized V get(Object key) {...} public synchronized V put(K key, V value) {...} public synchronized V remove(Object key) {...} public synchronized V replace(K key, V value) {...} ...

第二種方法是返回一個(gè)SynchronizedMap對(duì)象,這個(gè)對(duì)象默認(rèn)每個(gè)方法會(huì)鎖住整個(gè)對(duì)象。如下源碼:

什么是HashMap

img

這里的mutex是什么呢?直接看到構(gòu)造器:

final Object      mutex;        // Object on which to synchronize SynchronizedMap(Map<K,V> m) {     this.m = Objects.requireNonNull(m);     // 默認(rèn)為本對(duì)象     mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) {     this.m = m;     this.mutex = mutex; }

可以看到默認(rèn)鎖的就是本身,效果和Hashtable其實(shí)是一樣的。這種簡(jiǎn)單粗暴鎖整個(gè)對(duì)象的方式造成的后果是:

  • 鎖是非常重量級(jí)的,會(huì)嚴(yán)重影響性能。

  • 同一時(shí)間只能有一個(gè)線程進(jìn)行讀寫,限制了并發(fā)效率。

ConcurrentHashMap的設(shè)計(jì)就是為了解決此問題。他通過降低鎖粒度+CAS的方式來提高效率。簡(jiǎn)單來說,ConcurrentHashMap鎖的并不是整個(gè)對(duì)象,而是一個(gè)數(shù)組的一個(gè)節(jié)點(diǎn),那么其他線程訪問數(shù)組其他節(jié)點(diǎn)是不會(huì)互相影響,極大提高了并發(fā)效率;同時(shí)ConcurrentHashMap讀操作并不需要獲取鎖,如下圖:

什么是HashMap

img

關(guān)于ConcurrentHashMap和Hashtable的更多內(nèi)容,限于篇幅,我會(huì)在另一篇文章講解。那么,使用了上述的三種解決方案是不是絕對(duì)線程安全?先觀察下面的代碼:

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); map.put("abc","123");  Thread1: if (map.containsKey("abc")){     String s = map.get("abc"); }  Thread2: map.remove("abc");

當(dāng)Thread1調(diào)用containsKey之后釋放鎖,Thread2獲得鎖并把“abc”移除再釋放鎖,這個(gè)時(shí)候Thread1讀取到的s就是一個(gè)null了,也就出現(xiàn)了問題了。所以ConcurrentHashMap類或者Collections.synchronizeMap()方法或者Hashtable都只能在一定的限度上保證線程安全,而無法保證絕對(duì)線程安全。

關(guān)于線程安全,還有一個(gè)fast-fail問題,即快速失敗。當(dāng)使用HashMap的迭代器遍歷HashMap時(shí),如果此時(shí)HashMap發(fā)生了結(jié)構(gòu)性改變,如插入新數(shù)據(jù)、移除數(shù)據(jù)、擴(kuò)容等,那么Iteractor會(huì)拋出fast-fail異常,防止出現(xiàn)并發(fā)異常,在一定限度上保證了線程安全。如下源碼:

final Node<K,V> nextNode() {     ...     if (modCount != expectedModCount)         throw new ConcurrentModificationException();    ... }

創(chuàng)建Iteractor對(duì)象時(shí)會(huì)記錄HashMap的modCount變量,每當(dāng)HashMap發(fā)生結(jié)構(gòu)性改變時(shí),modCount會(huì)加1。在迭代時(shí)判斷HashMap的modCount和自己保存的expectedModCount是否一致即可判斷是否發(fā)生了結(jié)構(gòu)性改變。

fast-fail異常只能當(dāng)做遍歷時(shí)的一種安全保證,而不能當(dāng)做多線程并發(fā)訪問HashMap的手段。若有并發(fā)需求,還是需要使用上述的三種方法。

小結(jié)

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. HashMap并不能保證線程安全,在多線程并發(fā)訪問下會(huì)出現(xiàn)意想不到的問題,如數(shù)據(jù)丟失等

  3. HashMap1.8采用尾插法進(jìn)行擴(kuò)容,防止出現(xiàn)鏈表環(huán)導(dǎo)致的死循環(huán)問題

  4. 解決并發(fā)問題的的方案有Hashtable、Collections.synchronizeMap()、ConcurrentHashMap。其中最佳解決方案是ConcurrentHashMap

  5. 上述解決方案并不能完全保證線程安全

  6. 快速失敗是HashMap迭代機(jī)制中的一種并發(fā)安全保證

源碼解析

關(guān)鍵變量的理解

HashMap源碼中有很多的內(nèi)部變量,這些變量會(huì)在下面源碼分析中經(jīng)常出現(xiàn),首先需要理解這些變量的意義。

// 存放數(shù)據(jù)的數(shù)組 transient Node<K,V>[] table; // 存儲(chǔ)的鍵值對(duì)數(shù)目 transient int size; // HashMap結(jié)構(gòu)修改的次數(shù),主要用于判斷fast-fail transient int modCount; // 最大限度存儲(chǔ)鍵值對(duì)的數(shù)目(threshodl=table.length*loadFactor),也稱為閾值 int threshold; // 裝載因子,表示可最大容納數(shù)據(jù)數(shù)量的比例 final float loadFactor; // 靜態(tài)內(nèi)部類,HashMap存儲(chǔ)的節(jié)點(diǎn)類型;可存儲(chǔ)鍵值對(duì),本身是個(gè)鏈表結(jié)構(gòu)。 static class Node<K,V> implements Map.Entry<K,V> {...}

擴(kuò)容

HashMap源碼中把初始化操作也放到了擴(kuò)容方法中,因而擴(kuò)容方法源碼主要分為兩部分:確定新的數(shù)組大小、遷移數(shù)據(jù)。詳細(xì)的源碼分析如下,我打了非常詳細(xì)的注釋,可選擇查看。擴(kuò)容的步驟在上述已經(jīng)講過了,讀者可以自行結(jié)合源碼,分析HashMap是如何實(shí)現(xiàn)上述的設(shè)計(jì)。

final Node<K,V>[] resize() {     // 變量分別是原數(shù)組、原數(shù)組大小、原閾值;新數(shù)組大小、新閾值     Node<K,V>[] oldTab = table;     int oldCap = (oldTab == null) ? 0 : oldTab.length;     int oldThr = threshold;     int newCap, newThr = 0;      // 如果原數(shù)組長(zhǎng)度大于0     if (oldCap > 0) {         // 如果已經(jīng)超過了設(shè)置的最大長(zhǎng)度(1<<30,也就是最大整型正數(shù))         if (oldCap >= MAXIMUM_CAPACITY) {             // 直接把閾值設(shè)置為最大正數(shù)             threshold = Integer.MAX_VALUE;             return oldTab;         }         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                  oldCap >= DEFAULT_INITIAL_CAPACITY)             // 設(shè)置為原來的兩倍             newThr = oldThr << 1;      }      // 原數(shù)組長(zhǎng)度為0,但最大限度不是0,把長(zhǎng)度設(shè)置為閾值     // 對(duì)應(yīng)的情況就是新建HashMap的時(shí)候指定了數(shù)組長(zhǎng)度     else if (oldThr > 0)          newCap = oldThr;     // 第一次初始化,默認(rèn)16和0.75     // 對(duì)應(yīng)使用默認(rèn)構(gòu)造器新建HashMap對(duì)象     else {                        newCap = DEFAULT_INITIAL_CAPACITY;         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);     }     // 如果原數(shù)組長(zhǎng)度小于16或者翻倍之后超過了最大限制長(zhǎng)度,則重新計(jì)算閾值     if (newThr == 0) {         float ft = (float)newCap * loadFactor;         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                   (int)ft : Integer.MAX_VALUE);     }     threshold = newThr;      @SuppressWarnings({"rawtypes","unchecked"})     // 建立新的數(shù)組     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];     table = newTab;     if (oldTab != null) {         // 循環(huán)遍歷原數(shù)組,并給每個(gè)節(jié)點(diǎn)計(jì)算新的位置         for (int j = 0; j < oldCap; ++j) {             Node<K,V> e;             if ((e = oldTab[j]) != null) {                 oldTab[j] = null;                 // 如果他沒有后繼節(jié)點(diǎn),那么直接使用新的數(shù)組長(zhǎng)度取模得到新下標(biāo)                 if (e.next == null)                     newTab[e.hash & (newCap - 1)] = e;                 // 如果是紅黑樹,調(diào)用紅黑樹的拆解方法                 else if (e instanceof TreeNode)                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                 // 新的位置只有兩種可能:原位置,原位置+老數(shù)組長(zhǎng)度                 // 把原鏈表拆成兩個(gè)鏈表,然后再分別插入到新數(shù)組的兩個(gè)位置上                 // 不用多次調(diào)用put方法                 else {                      // 分別是原位置不變的鏈表和原位置+原數(shù)組長(zhǎng)度位置的鏈表                     Node<K,V> loHead = null, loTail = null;                     Node<K,V> hiHead = null, hiTail = null;                     Node<K,V> next;                     // 遍歷老鏈表,判斷新增判定位是1or0進(jìn)行分類                     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);                     // 最后賦值給新的數(shù)組                     if (loTail != null) {                         loTail.next = null;                         newTab[j] = loHead;                     }                     if (hiTail != null) {                         hiTail.next = null;                         newTab[j + oldCap] = hiHead;                     }                 }             }         }     }     // 返回新數(shù)組     return newTab; }

添加數(shù)值

調(diào)用put()方法添加鍵值對(duì),最終會(huì)調(diào)用putVal()來真正實(shí)現(xiàn)添加邏輯。代碼解析如下:

public V put(K key, V value) {     // 獲取hash值,再調(diào)用putVal方法插入數(shù)據(jù)     return putVal(hash(key), key, value, false, true); }  // onlyIfAbsent表示是否覆蓋舊值,true表示不覆蓋,false表示覆蓋,默認(rèn)為false // evict和LinkHashMap的回調(diào)方法有關(guān),不在本文討論范圍 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                boolean evict) {      // tab是HashMap內(nèi)部數(shù)組,n是數(shù)組的長(zhǎng)度,i是要插入的下標(biāo),p是該下標(biāo)對(duì)應(yīng)的節(jié)點(diǎn)     Node<K,V>[] tab; Node<K,V> p; int n, i;      // 判斷數(shù)組是否是null或者是否是空,若是,則調(diào)用resize()方法進(jìn)行擴(kuò)容     if ((tab = table) == null || (n = tab.length) == 0)         n = (tab = resize()).length;      // 使用位與運(yùn)算代替取模得到下標(biāo)     // 判斷當(dāng)前下標(biāo)是否是null,若是則創(chuàng)建節(jié)點(diǎn)直接插入,若不是,進(jìn)入下面else邏輯     if ((p = tab[i = (n - 1) & hash]) == null)         tab[i] = newNode(hash, key, value, null);     else {          // e表示和當(dāng)前key相同的節(jié)點(diǎn),若不存在該節(jié)點(diǎn)則為null         // k是當(dāng)前數(shù)組下標(biāo)節(jié)點(diǎn)的key         Node<K,V> e; K k;          // 判斷當(dāng)前節(jié)點(diǎn)與要插入的key是否相同,是則表示找到了已經(jīng)存在的key         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))             e = p;         // 判斷該節(jié)點(diǎn)是否是樹節(jié)點(diǎn),如果是調(diào)用紅黑樹的方法進(jìn)行插入         else if (p instanceof TreeNode)             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);         // 最后一種情況是直接鏈表插入         else {             for (int binCount = 0; ; ++binCount) {                 if ((e = p.next) == null) {                     p.next = newNode(hash, key, value, null);                     // 長(zhǎng)度大于等于8時(shí)轉(zhuǎn)化為紅黑樹                     // 注意,treeifyBin方法中會(huì)進(jìn)行數(shù)組長(zhǎng)度判斷,                     // 若小于64,則優(yōu)先進(jìn)行數(shù)組擴(kuò)容而不是轉(zhuǎn)化為樹                     if (binCount >= TREEIFY_THRESHOLD - 1)                          treeifyBin(tab, hash);                     break;                 }                 // 找到相同的直接跳出循環(huán)                 if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                     break;                 p = e;             }         }          // 如果找到相同的key節(jié)點(diǎn),則判斷onlyIfAbsent和舊值是否為null         // 執(zhí)行更新或者不操作,最后返回舊值         if (e != null) {              V oldValue = e.value;             if (!onlyIfAbsent || oldValue == null)                 e.value = value;             afterNodeAccess(e);             return oldValue;         }     }      // 如果不是更新舊值,說明HashMap中鍵值對(duì)數(shù)量發(fā)生變化     // modCount數(shù)值+1表示結(jié)構(gòu)改變     ++modCount;     // 判斷長(zhǎng)度是否達(dá)到最大限度,如果是則進(jìn)行擴(kuò)容     if (++size > threshold)         resize();     // 最后返回null(afterNodeInsertion是LinkHashMap的回調(diào))     afterNodeInsertion(evict);     return null; }

代碼中關(guān)于每個(gè)步驟有了詳細(xì)的解釋,這里來總結(jié)一下:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 總體上分為兩種情況:找到相同的key和找不到相同的key。找了需要判斷是否更新并返回舊value,沒找到需要插入新的Node、更新節(jié)點(diǎn)數(shù)并判斷是否需要擴(kuò)容。

  3. 查找分為三種情況:數(shù)組、鏈表、紅黑樹。數(shù)組下標(biāo)i位置不為空且不等于key,那么就需要判斷是否樹節(jié)點(diǎn)還是鏈表節(jié)點(diǎn)并進(jìn)行查找。

  4. 鏈表到達(dá)一定長(zhǎng)度后需要擴(kuò)展為紅黑樹,當(dāng)且僅當(dāng)鏈表長(zhǎng)度>=8且數(shù)組長(zhǎng)度>=64。

最后畫一張圖總體再加深一下整個(gè)流程的印象:

什么是HashMap

img

其他問題

為什么jdk1.7以前控制數(shù)組的長(zhǎng)度為素?cái)?shù),而jdk1.8之后卻采用的是2的整數(shù)次冪?

答:素?cái)?shù)長(zhǎng)度可以有效減少哈希沖突;JDK1.8之后采用2的整數(shù)次冪是為了提高求余和擴(kuò)容的效率,同時(shí)結(jié)合高低位異或的方法使得哈希散列更加均勻。

為何素?cái)?shù)可以減少哈希沖突?若能保證key的hashcode在每個(gè)數(shù)字之間都是均勻分布,那么無論是素?cái)?shù)還是合數(shù)都是相同的效果。例如hashcode在1~20均勻分布,那么無論長(zhǎng)度是合數(shù)4,還是素?cái)?shù)5,分布都是均勻的。而如果hashcode之間的間隔都是2,如1,3,5...,那么長(zhǎng)度為4的數(shù)組,位置2和位置4兩個(gè)下標(biāo)無法放入數(shù)據(jù),而長(zhǎng)度為5的數(shù)組則沒有這個(gè)問題。長(zhǎng)度為合數(shù)的數(shù)組會(huì)使間隔為其因子的hashcode聚集出現(xiàn),從而使得散列效果降低。

為什么插入HashMap的數(shù)據(jù)需要實(shí)現(xiàn)hashcode和equals方法?對(duì)這兩個(gè)方法有什么要求?

答:通過hashcode來確定插入下標(biāo),通過equals比較來尋找數(shù)據(jù);兩個(gè)相等的key的hashcode必須相等,但擁有相同的hashcode的對(duì)象不一定相等。

這里需要區(qū)分好他們之間的區(qū)別:hashcode就像一個(gè)人的名,相同的人名字肯定相等,但是相同的名字不一定是同個(gè)人;equals比較內(nèi)容是否相同,一般由對(duì)象覆蓋重寫,默認(rèn)情況下比較的是引用地址;“==”引用隊(duì)形比較的是引用地址是否相同,值對(duì)象比較的是值是否相同。

HashMap中需要使用hashcode來獲取key的下標(biāo),如果兩個(gè)相同的對(duì)象的hashcode不同,那么會(huì)造成HashMap中存在相同的key;所以equals返回相同的key他們的hashcode一定要相同。HashMap比較兩個(gè)元素是否相同采用了三種比較方法結(jié)合:p.hash  == hash && ((k = p.key) == key || (key != null &&  key.equals(k)))  。

看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI