<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【129期】看完這篇,再也不怕面試被問HashMap了~

          共 763字,需瀏覽 2分鐘

           ·

          2021-02-02 00:19

          程序員的成長之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 16?分鐘。

          作者:超大只烏龜
          https://segmentfault.com/a/1190000022184751

          總所周知 HashMap 是面試中經(jīng)常問到的一個知識點,也是判斷一個候選人基礎(chǔ)是否扎實的標(biāo)準(zhǔn)之一,因為通過?HashMap?可以引出很多知識點,比如數(shù)據(jù)結(jié)構(gòu)(數(shù)組、鏈表、紅黑樹)、equals 和 hashcode?方法。


          除此之外還可以引出線程安全的問題,HashMap?是我在初學(xué)階段學(xué)到的設(shè)計的最為巧妙的集合,里面有很多細(xì)節(jié)以及優(yōu)化技巧都值得我們深入學(xué)習(xí),話不多說先看看相關(guān)的面試題:

          ? ?默認(rèn)大小、負(fù)載因子以及擴(kuò)容倍數(shù)是多少

          ? ?底層數(shù)據(jù)結(jié)構(gòu)

          ? ?如何處理 hash 沖突的

          ? ?如何計算一個 key 的 hash 值

          ? ?數(shù)組長度為什么是 2 的冪次方

          ? ?擴(kuò)容、查找過程

          如果上面的都能回答出來的話你就不需要看這篇文章了,那么開始進(jìn)入正文。

          數(shù)據(jù)結(jié)構(gòu)

          ? ?在 JDK1.8 中,HashMap 是由數(shù)組+鏈表+紅黑樹構(gòu)成

          ? ?當(dāng)一個值中要存儲到 HashMap 中的時候會根據(jù) Key 的值來計算出他的 hash,通過 hash 值來確認(rèn)存放到數(shù)組中的位置,如果發(fā)生 hash 沖突就以鏈表的形式存儲,當(dāng)鏈表過長的話,HashMap 會把這個鏈表轉(zhuǎn)換成紅黑樹來存儲。

          在看源碼之前我們需要先看看一些基本屬性

          //默認(rèn)初始容量為16??
          static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<4;??
          //默認(rèn)負(fù)載因子為0.75??
          static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;??
          //Hash數(shù)組(在resize()中初始化)??
          transient?Node[]?table;??
          //元素個數(shù)??
          transient?int?size;??
          //容量閾值(元素個數(shù)超過該值會自動擴(kuò)容)??
          int?threshold;

          table 數(shù)組里面存放的是 Node 對象,Node 是 HashMap 的一個內(nèi)部類,用來表示一個 key-value,源碼如下:

          static?class?Node<K,V>?implements?Map.Entry<K,V>?{??
          ????final?int?hash;??
          ????final?K?key;??
          ????V?value;??
          ????Node?next;??

          ????Node(int?hash,?K?key,?V?value,?Node?next)?{??
          ????????this.hash?=?hash;??
          ????????this.key?=?key;??
          ????????this.value?=?value;??
          ????????this.next?=?next;??
          ????}??

          ????public?final?K?getKey()????????{?return?key;?}??
          ????public?final?V?getValue()??????{?return?value;?}??
          ????public?final?String?toString()?{?return?key?+?"="?+?value;?}??
          ????public?final?int?hashCode()?{??
          ????????return?Objects.hashCode(key)?^?Objects.hashCode(value);//^表示相同返回0,不同返回1??
          ????????//Objects.hashCode(o)————>return?o?!=?null???o.hashCode()?:?0;??
          ????}??

          ????public?final?V?setValue(V?newValue)?{??
          ????????V?oldValue?=?value;??
          ????????value?=?newValue;??
          ????????return?oldValue;??
          ????}??

          ????public?final?boolean?equals(Object?o)?{??
          ????????if?(o?==?this)??
          ????????????return?true;??
          ????????if?(o?instanceof?Map.Entry)?{??
          ????????????Map.Entry?e?=?(Map.Entry)o;??
          ????????????//Objects.equals(1,b)————>?return?(a?==?b)?||?(a?!=?null?&&?a.equals(b));??
          ????????????if?(Objects.equals(key,?e.getKey())?&&?Objects.equals(value,?e.getValue()))??
          ????????????????return?true;??
          ????????}??
          ????????return?false;??
          ????}??
          }

          總結(jié):

          ? ?默認(rèn)初始容量為 16,默認(rèn)負(fù)載因子為 0.75

          ? ?threshold = 數(shù)組長度 * loadFactor,當(dāng)元素個數(shù)超過threshold(容量閾值)時,HashMap 會進(jìn)行擴(kuò)容操作

          ? ?table 數(shù)組中存放指向鏈表的引用

          這里需要注意的一點是 table 數(shù)組并不是在構(gòu)造方法里面初始化的,它是在 resize(擴(kuò)容)方法里進(jìn)行初始化的。

          table 數(shù)組長度永遠(yuǎn)為 2 的冪次方

          總所周知,HashMap 數(shù)組長度永遠(yuǎn)為 2 的冪次方(指的是 table 數(shù)組的大小),那你有想過為什么嗎?

          首先我們需要知道 HashMap 是通過一個名為 tableSizeFor 的方法來確保 HashMap 數(shù)組長度永遠(yuǎn)為2的冪次方的,源碼如下:

          /*找到大于或等于?cap?的最小2的冪,用來做容量閾值*/??
          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 的功能(不考慮大于最大容量的情況)是返回大于等于輸入?yún)?shù)且最近的 2 的整數(shù)次冪的數(shù)。比如 10,則返回 16。

          該算法讓最高位的 1 后面的位全變?yōu)?1。最后再讓結(jié)果 n+1,即得到了 2 的整數(shù)次冪的值了。

          讓 cap-1 再賦值給 n 的目的是另找到的目標(biāo)值大于或等于原值。例如二進(jìn)制 1000,十進(jìn)制數(shù)值為 8。如果不對它減1而直接操作,將得到答案 10000,即 16。顯然不是結(jié)果。減 1 后二進(jìn)制為 111,再進(jìn)行操作則會得到原來的數(shù)值 1000,即 8。通過一系列位運算大大提高效率。

          那在什么地方會用到 tableSizeFor 方法呢?

          答案就是在構(gòu)造方法里面調(diào)用該方法來設(shè)置 threshold,也就是容量閾值。

          這里你可能又會有一個疑問:為什么要設(shè)置為 threshold 呢?

          因為在擴(kuò)容方法里第一次初始化 table 數(shù)組時會將 threshold 設(shè)置數(shù)組的長度,后續(xù)在講擴(kuò)容方法時再介紹。

          /*傳入初始容量和負(fù)載因子*/??
          public?HashMap(int?initialCapacity,?float?loadFactor)?{??

          ????if?(initialCapacity?0)??
          ????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+initialCapacity);??
          ????if?(initialCapacity?>?MAXIMUM_CAPACITY)??
          ????????initialCapacity?=?MAXIMUM_CAPACITY;??
          ????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))??
          ????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+loadFactor);??

          ????this.loadFactor?=?loadFactor;??
          ????this.threshold?=?tableSizeFor(initialCapacity);??
          }

          那么為什么要把數(shù)組長度設(shè)計為 2 的冪次方呢?

          我個人覺得這樣設(shè)計有以下幾個好處:

          1. 當(dāng)數(shù)組長度為 2 的冪次方時,可以使用位運算來計算元素在數(shù)組中的下標(biāo)

          HashMap 是通過 index=hash&(table.length-1) 這條公式來計算元素在 table 數(shù)組中存放的下標(biāo),就是把元素的 hash 值和數(shù)組長度減1的值做一個與運算,即可求出該元素在數(shù)組中的下標(biāo),這條公式其實等價于 hash%length,也就是對數(shù)組長度求模取余,只不過只有當(dāng)數(shù)組長度為 2 的冪次方時,hash&(length-1) 才等價于 hash%length,使用位運算可以提高效率。

          2. 增加 hash 值的隨機(jī)性,減少 hash 沖突

          如果 length 為 2 的冪次方,則 length-1 轉(zhuǎn)化為二進(jìn)制必定是 11111……的形式,這樣的話可以使所有位置都能和元素 hash 值做與運算,如果是如果 length 不是 2 的次冪,比如 length 為 15,則 length-1 為 14,對應(yīng)的二進(jìn)制為 1110,在和 hash 做與運算時,最后一位永遠(yuǎn)都為 0 ,浪費空間。

          擴(kuò)容

          HashMap 每次擴(kuò)容都是建立一個新的 table 數(shù)組,長度和容量閾值都變?yōu)樵瓉淼膬杀?,然后把原?shù)組元素重新映射到新數(shù)組上,具體步驟如下:

          1. 首先會判斷 table 數(shù)組長度,如果大于 0 說明已被初始化過,那么按當(dāng)前 table 數(shù)組長度的 2 倍進(jìn)行擴(kuò)容,閾值也變?yōu)樵瓉淼?2 倍

          2. 若 table 數(shù)組未被初始化過,且 threshold(閾值)大于 0 說明調(diào)用了 HashMap(initialCapacity, loadFactor) 構(gòu)造方法,那么就把數(shù)組大小設(shè)為 threshold

          3. 若 table 數(shù)組未被初始化,且 threshold 為 0 說明調(diào)用 HashMap() 構(gòu)造方法,那么就把數(shù)組大小設(shè)為 16,threshold 設(shè)為 16*0.75

          4. 接著需要判斷如果不是第一次初始化,那么擴(kuò)容之后,要重新計算鍵值對的位置,并把它們移動到合適的位置上去,如果節(jié)點是紅黑樹類型的話則需要進(jìn)行紅黑樹的拆分。

          這里有一個需要注意的點就是在 JDK1.8 HashMap 擴(kuò)容階段重新映射元素時不需要像 1.7 版本那樣重新去一個個計算元素的 hash 值,而是通過 hash & oldCap 的值來判斷,若為 0 則索引位置不變,不為 0 則新索引=原索引+舊數(shù)組長度,為什么呢?具體原因如下:

          因為我們使用的是 2 次冪的擴(kuò)展(指長度擴(kuò)為原來 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移動 2 次冪的位置。因此,我們在擴(kuò)充 HashMap 的時候,不需要像 JDK1.7 的實現(xiàn)那樣重新計算 hash,只需要看看原來的 hash 值新增的那個 bit 是 1 還是 0 就好了,是 0 的話索引沒變,是 1 的話索引變成“原索引 +oldCap

          這點其實也可以看做長度為 2 的冪次方的一個好處,也是 HashMap 1.7 和 1.8 之間的一個區(qū)別,具體源碼如下:

          /*擴(kuò)容*/??
          final?Node[]?resize()?{??
          ????Node[]?oldTab?=?table;??
          ????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;??
          ????int?oldThr?=?threshold;??
          ????int?newCap,?newThr?=?0;??

          ????//1、若oldCap>0?說明hash數(shù)組table已被初始化??
          ????if?(oldCap?>?0)?{??
          ????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{??
          ????????????threshold?=?Integer.MAX_VALUE;??
          ????????????return?oldTab;??
          ????????}//按當(dāng)前table數(shù)組長度的2倍進(jìn)行擴(kuò)容,閾值也變?yōu)樵瓉淼?倍??
          ????????else?if?((newCap?=?oldCap?<1)?=?DEFAULT_INITIAL_CAPACITY)??
          ????????????newThr?=?oldThr?<1;??
          ????}//2、若數(shù)組未被初始化,而threshold>0說明調(diào)用了HashMap(initialCapacity)和HashMap(initialCapacity,?loadFactor)構(gòu)造器??
          ????else?if?(oldThr?>?0)??
          ????????newCap?=?oldThr;//新容量設(shè)為數(shù)組閾值??
          ????else?{?//3、若table數(shù)組未被初始化,且threshold為0說明調(diào)用HashMap()構(gòu)造方法??
          ????????newCap?=?DEFAULT_INITIAL_CAPACITY;//默認(rèn)為16??
          ????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);//16*0.75??
          ????}??

          ????//若計算過程中,閾值溢出歸零,則按閾值公式重新計算??
          ????if?(newThr?==?0)?{??
          ????????float?ft?=?(float)newCap?*?loadFactor;??
          ????????newThr?=?(newCap?float)MAXIMUM_CAPACITY????
          ??????????????????(int)ft?:?Integer.MAX_VALUE);??
          ????}??
          ????threshold?=?newThr;??
          ????//創(chuàng)建新的hash數(shù)組,hash數(shù)組的初始化也是在這里完成的??
          ????Node[]?newTab?=?(Node[])new?Node[newCap];??
          ????table?=?newTab;??
          ????//如果舊的hash數(shù)組不為空,則遍歷舊數(shù)組并映射到新的hash數(shù)組??
          ????if?(oldTab?!=?null)?{??
          ????????for?(int?j?=?0;?j?????????????Node?e;??
          ????????????if?((e?=?oldTab[j])?!=?null)?{??
          ????????????????oldTab[j]?=?null;//GC??
          ????????????????if?(e.next?==?null)//如果只鏈接一個節(jié)點,重新計算并放入新數(shù)組??
          ????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;??
          ????????????????//若是紅黑樹,則需要進(jìn)行拆分??
          ????????????????else?if?(e?instanceof?TreeNode)??
          ????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);??
          ????????????????else?{??
          ????????????????????//rehash————>重新映射到新數(shù)組??
          ????????????????????Node?loHead?=?null,?loTail?=?null;??
          ????????????????????Node?hiHead?=?null,?hiTail?=?null;??
          ????????????????????Node?next;??
          ????????????????????do?{??
          ????????????????????????next?=?e.next;??
          ????????????????????????/*注意這里使用的是:e.hash & oldCap,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長度*/??
          ????????????????????????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;??
          ????????????????????}??
          ????????????????}??
          ????????????}??
          ????????}??
          ????}??
          ????return?newTab;??
          }

          在擴(kuò)容方法里面還涉及到有關(guān)紅黑樹的幾個知識點:

          鏈表樹化

          指的就是把鏈表轉(zhuǎn)換成紅黑樹,樹化需要滿足以下兩個條件:

          • 鏈表長度大于等于 8

          • table 數(shù)組長度大于等于 64

          為什么 table 數(shù)組容量大于等于 64 才樹化?

          因為當(dāng) table 數(shù)組容量比較小時,鍵值對節(jié)點 hash 的碰撞率可能會比較高,進(jìn)而導(dǎo)致鏈表長度較長。這個時候應(yīng)該優(yōu)先擴(kuò)容,而不是立馬樹化。

          紅黑樹拆分

          拆分就是指擴(kuò)容后對元素重新映射時,紅黑樹可能會被拆分成兩條鏈表。

          由于篇幅有限,有關(guān)紅黑樹這里就不展開了。

          查找

          HashMap 的查找是非常快的,要查找一個元素首先得知道 key 的 hash 值,在 HashMap 中并不是直接通過 key 的 hashcode 方法獲取哈希值,而是通過內(nèi)部自定義的 hash 方法計算哈希值,我們來看看其實現(xiàn):

          static?final?int?hash(Object?key)?{??
          ????int?h;??
          ????return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);??
          }

          (h = key.hashCode()) ^ (h >>> 16) 是為了讓高位數(shù)據(jù)與低位數(shù)據(jù)進(jìn)行異或,變相的讓高位數(shù)據(jù)參與到計算中,int 有 32 位,右移 16 位就能讓低 16 位和高 16 位進(jìn)行異或,也是為了增加 hash 值的隨機(jī)性。

          知道如何計算 hash 值后我們來看看 get 方法

          public?V?get(Object?key)?{??
          ????Node?e;??
          ????return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;//hash(key)不等于key.hashCode??
          }??

          final?Node?getNode(int?hash,?Object?key)?{??
          ????Node[]?tab;?//指向hash數(shù)組??
          ????Node?first,?e;?//first指向hash數(shù)組鏈接的第一個節(jié)點,e指向下一個節(jié)點??
          ????int?n;//hash數(shù)組長度??
          ????K?k;??
          ????/*(n?-?1)?&?hash?————>根據(jù)hash值計算出在數(shù)組中的索引index(相當(dāng)于對數(shù)組長度取模,這里用位運算進(jìn)行了優(yōu)化)*/??
          ????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(first?=?tab[(n?-?1)?&?hash])?!=?null)?{??
          ????????//基本類型用==比較,其它用euqals比較??
          ????????if?(first.hash?==?hash?&&?((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??
          ????????????return?first;??
          ????????if?((e?=?first.next)?!=?null)?{??
          ????????????//如果first是TreeNode類型,則調(diào)用紅黑樹查找方法??
          ????????????if?(first?instanceof?TreeNode)??
          ????????????????return?((TreeNode)first).getTreeNode(hash,?key);??
          ????????????do?{//向后遍歷??
          ????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??
          ????????????????????return?e;??
          ????????????}?while?((e?=?e.next)?!=?null);??
          ????????}??
          ????}??
          ????return?null;??
          }`

          這里要注意的一點就是在 HashMap 中用 (n - 1) & hash 計算 key 所對應(yīng)的索引 index(相當(dāng)于對數(shù)組長度取模,這里用位運算進(jìn)行了優(yōu)化),這點在上面已經(jīng)說過了,就不再廢話了。

          插入

          我們先來看看插入元素的步驟:

          1. 當(dāng) table 數(shù)組為空時,通過擴(kuò)容的方式初始化 table

          2. 通過計算鍵的 hash 值求出下標(biāo)后,若該位置上沒有元素(沒有發(fā)生 hash 沖突),則新建 Node 節(jié)點插入

          3. 若發(fā)生了 hash 沖突,遍歷鏈表查找要插入的 key 是否已經(jīng)存在,存在的話根據(jù)條件判斷是否用新值替換舊值

          4. 如果不存在,則將元素插入鏈表尾部,并根據(jù)鏈表長度決定是否將鏈表轉(zhuǎn)為紅黑樹

          5. 判斷鍵值對數(shù)量是否大于閾值,大于的話則進(jìn)行擴(kuò)容操作

          先看完上面的流程,再來看源碼會簡單很多,源碼如下:

          public?V?put(K?key,?V?value)?{??
          ????return?putVal(hash(key),?key,?value,?false,?true);??
          }??

          final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,boolean?evict)?{??
          ????Node[]?tab;//指向hash數(shù)組??
          ????Node?p;//初始化為table中第一個節(jié)點??
          ????int?n,?i;//n為數(shù)組長度,i為索引??

          ????//tab被延遲到插入新數(shù)據(jù)時再進(jìn)行初始化??
          ????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)??
          ????????n?=?(tab?=?resize()).length;??
          ????//如果數(shù)組中不包含Node引用,則新建Node節(jié)點存入數(shù)組中即可??
          ????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)??
          ????????tab[i]?=?newNode(hash,?key,?value,?null);//new?Node<>(hash,?key,?value,?next)??
          ????else?{??
          ????????Node?e;?//如果要插入的key-value已存在,用e指向該節(jié)點??
          ????????K?k;??
          ????????//如果第一個節(jié)點就是要插入的key-value,則讓e指向第一個節(jié)點(p在這里指向第一個節(jié)點)??
          ????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??
          ????????????e?=?p;??
          ????????//如果p是TreeNode類型,則調(diào)用紅黑樹的插入操作(注意:TreeNode是Node的子類)??
          ????????else?if?(p?instanceof?TreeNode)??
          ????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);??
          ????????else?{??
          ????????????//對鏈表進(jìn)行遍歷,并用binCount統(tǒng)計鏈表長度??
          ????????????for?(int?binCount?=?0;?;?++binCount)?{??
          ????????????????//如果鏈表中不包含要插入的key-value,則將其插入到鏈表尾部??
          ????????????????if?((e?=?p.next)?==?null)?{??
          ????????????????????p.next?=?newNode(hash,?key,?value,?null);??
          ????????????????????//如果鏈表長度大于或等于樹化閾值,則進(jìn)行樹化操作??
          ????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)??
          ????????????????????????treeifyBin(tab,?hash);??
          ????????????????????break;??
          ????????????????}??
          ????????????????//如果要插入的key-value已存在則終止遍歷,否則向后遍歷??
          ????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??
          ????????????????????break;??
          ????????????????p?=?e;??
          ????????????}??
          ????????}??
          ????????//如果e不為null說明要插入的key-value已存在??
          ????????if?(e?!=?null)?{??
          ????????????V?oldValue?=?e.value;??
          ????????????//根據(jù)傳入的onlyIfAbsent判斷是否要更新舊值??
          ????????????if?(!onlyIfAbsent?||?oldValue?==?null)??
          ????????????????e.value?=?value;??
          ????????????afterNodeAccess(e);??
          ????????????return?oldValue;??
          ????????}??
          ????}??
          ????++modCount;??
          ????//鍵值對數(shù)量超過閾值時,則進(jìn)行擴(kuò)容??
          ????if?(++size?>?threshold)??
          ????????resize();??
          ????afterNodeInsertion(evict);//也是空函數(shù)?回調(diào)?不知道干嘛的??
          ????return?null;??
          }

          從源碼也可以看出 table 數(shù)組是在第一次調(diào)用 put 方法后才進(jìn)行初始化的。

          刪除

          HashMap 的刪除操作并不復(fù)雜,僅需三個步驟即可完成。

          1. 定位桶位置

          2. 遍歷鏈表找到相等的節(jié)點

          3. 第三步刪除節(jié)點

          public?V?remove(Object?key)?{??
          ????Node?e;??
          ????return?(e?=?removeNode(hash(key),?key,?null,?false,?true))?==?null???null?:?e.value;??
          }??

          final?Node?removeNode(int?hash,?Object?key,?Object?value,boolean?matchValue,?boolean?movable)?{??
          ????Node[]?tab;??
          ????Node?p;??
          ????int?n,?index;??
          ????//1、定位元素桶位置??
          ????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(p?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{??
          ????????Node?node?=?null,?e;??
          ????????K?k;??
          ????????V?v;??
          ????????//?如果鍵的值與鏈表第一個節(jié)點相等,則將?node?指向該節(jié)點??
          ????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??
          ????????????node?=?p;??
          ????????else?if?((e?=?p.next)?!=?null)?{??
          ????????????//?如果是?TreeNode?類型,調(diào)用紅黑樹的查找邏輯定位待刪除節(jié)點??
          ????????????if?(p?instanceof?TreeNode)??
          ????????????????node?=?((TreeNode)p).getTreeNode(hash,?key);??
          ????????????else?{??
          ????????????????//?2、遍歷鏈表,找到待刪除節(jié)點??
          ????????????????do?{??
          ????????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))?{??
          ????????????????????????node?=?e;??
          ????????????????????????break;??
          ????????????????????}??
          ????????????????????p?=?e;??
          ????????????????}?while?((e?=?e.next)?!=?null);??
          ????????????}??
          ????????}??
          ????????//?3、刪除節(jié)點,并修復(fù)鏈表或紅黑樹??
          ????????if?(node?!=?null?&&?(!matchValue?||?(v?=?node.value)?==?value?||?(value?!=?null?&&?value.equals(v))))?{??
          ????????????if?(node?instanceof?TreeNode)??
          ????????????????((TreeNode)node).removeTreeNode(this,?tab,?movable);??
          ????????????else?if?(node?==?p)??
          ????????????????tab[index]?=?node.next;??
          ????????????else??
          ????????????????p.next?=?node.next;??
          ????????????++modCount;??
          ????????????--size;??
          ????????????afterNodeRemoval(node);??
          ????????????return?node;??
          ????????}??
          ????}??
          ????return?null;??
          }

          注意:刪除節(jié)點后可能破壞了紅黑樹的平衡性質(zhì),removeTreeNode 方法會對紅黑樹進(jìn)行變色、旋轉(zhuǎn)等操作來保持紅黑樹的平衡結(jié)構(gòu),這部分比較復(fù)雜。

          遍歷

          在工作中 HashMap 的遍歷操作也是非常常用的,也許有很多小伙伴喜歡用 for-each 來遍歷,但是你知道其中有哪些坑嗎?

          看下面的例子,當(dāng)我們在遍歷 HashMap 的時候,若使用 remove 方法刪除元素時會拋出 ConcurrentModificationException 異常

          Map?map?=?new?HashMap<>();??
          map.put("1",?1);??
          map.put("2",?2);??
          map.put("3",?3);??
          for?(String?s?:?map.keySet())?{??
          ????if?(s.equals("2"))??
          ????????map.remove("2");??
          }

          這就是常說的 fail-fast(快速失敗)機(jī)制,這個就需要從一個變量說起

          transient?int?modCount;

          在 HashMap 中有一個名為 modCount 的變量,它用來表示集合被修改的次數(shù),修改指的是插入元素或刪除元素,可以回去看看上面插入刪除的源碼,在最后都會對 modCount 進(jìn)行自增。

          當(dāng)我們在遍歷 HashMap 時,每次遍歷下一個元素前都會對 modCount 進(jìn)行判斷,若和原來的不一致說明集合結(jié)果被修改過了,然后就會拋出異常,這是 Java 集合的一個特性,我們這里以 keySet 為例,看看部分相關(guān)源碼:

          public?Set?keySet()?{??
          ????Set?ks?=?keySet;??
          ????if?(ks?==?null)?{??
          ????????ks?=?new?KeySet();??
          ????????keySet?=?ks;??
          ????}??
          ????return?ks;??
          }??

          final?class?KeySet?extends?AbstractSet<K>?{??
          ????public?final?Iterator?iterator()?????{?return?new?KeyIterator();?}??
          ????//?省略部分代碼??
          }??

          final?class?KeyIterator?extends?HashIterator?implements?Iterator<K>?{??
          ????public?final?K?next()?{?return?nextNode().key;?}??
          }??

          /*HashMap迭代器基類,子類有KeyIterator、ValueIterator等*/??
          abstract?class?HashIterator?{??
          ????Node?next;????????//下一個節(jié)點??
          ????Node?current;?????//當(dāng)前節(jié)點??
          ????int?expectedModCount;??//修改次數(shù)??
          ????int?index;?????????????//當(dāng)前索引??
          ????//無參構(gòu)造??
          ????HashIterator()?{??
          ????????expectedModCount?=?modCount;??
          ????????Node[]?t?=?table;??
          ????????current?=?next?=?null;??
          ????????index?=?0;??
          ????????//找到第一個不為空的桶的索引??
          ????????if?(t?!=?null?&&?size?>?0)?{??
          ????????????do?{}?while?(index?null);??
          ????????}??
          ????}??
          ????//是否有下一個節(jié)點??
          ????public?final?boolean?hasNext()?{??
          ????????return?next?!=?null;??
          ????}??
          ????//返回下一個節(jié)點??
          ????final?Node?nextNode()?{??
          ????????Node[]?t;??
          ????????Node?e?=?next;??
          ????????if?(modCount?!=?expectedModCount)??
          ????????????throw?new?ConcurrentModificationException();//fail-fast??
          ????????if?(e?==?null)??
          ????????????throw?new?NoSuchElementException();??
          ????????//當(dāng)前的鏈表遍歷完了就開始遍歷下一個鏈表??
          ????????if?((next?=?(current?=?e).next)?==?null?&&?(t?=?table)?!=?null)?{??
          ????????????do?{}?while?(index?null);??
          ????????}??
          ????????return?e;??
          ????}??
          ????//刪除元素??
          ????public?final?void?remove()?{??
          ????????Node?p?=?current;??
          ????????if?(p?==?null)??
          ????????????throw?new?IllegalStateException();??
          ????????if?(modCount?!=?expectedModCount)??
          ????????????throw?new?ConcurrentModificationException();??
          ????????current?=?null;??
          ????????K?key?=?p.key;??
          ????????removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode??
          ????????expectedModCount?=?modCount;??
          ????}??
          }

          相關(guān)代碼如下,可以看到若 modCount 被修改了則會拋出 ConcurrentModificationException 異常。

          if?(modCount?!=?expectedModCount)??
          ????throw?new?ConcurrentModificationException();

          那么如何在遍歷時刪除元素呢?

          我們可以看看迭代器自帶的 remove 方法,其中最后兩行代碼如下:

          `removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode??
          expectedModCount?=?modCount;
          `

          意思就是會調(diào)用外部 remove 方法刪除元素后,把 modCount 賦值給 expectedModCount,這樣的話兩者一致就不會拋出異常了,所以我們應(yīng)該這樣寫:

          Map?map?=?new?HashMap<>();??
          map.put("1",?1);??
          map.put("2",?2);??
          map.put("3",?3);??
          Iterator?iterator?=?map.keySet().iterator();??
          while?(iterator.hasNext()){??
          ????if?(iterator.next().equals("2"))??
          ????????iterator.remove();??
          }

          這里還有一個知識點就是在遍歷 HashMap 時,我們會發(fā)現(xiàn)遍歷的順序和插入的順序不一致,這是為什么?

          在 HashIterator 源碼里面可以看出,它是先從桶數(shù)組中找到包含鏈表節(jié)點引用的桶。然后對這個桶指向的鏈表進(jìn)行遍歷。遍歷完成后,再繼續(xù)尋找下一個包含鏈表節(jié)點引用的桶,找到繼續(xù)遍歷。找不到,則結(jié)束遍歷。這就解釋了為什么遍歷和插入的順序不一致,不懂的同學(xué)請看下圖:

          equasl 和 hashcode

          為什么添加到 HashMap 中的對象需要重寫 equals() 和 hashcode() 方法?

          簡單看個例子,這里以 Person 為例:

          public?class?Person?{??
          ????Integer?id;??
          ????String?name;??

          ????public?Person(Integer?id,?String?name)?{??
          ????????this.id?=?id;??
          ????????this.name?=?name;??
          ????}??

          ????@Override??
          ????public?boolean?equals(Object?obj)?{??
          ????????if?(obj?==?null)?return?false;??
          ????????if?(obj?==?this)?return?true;??
          ????????if?(obj?instanceof?Person)?{??
          ????????????Person?person?=?(Person)?obj;??
          ????????????if?(this.id?==?person.id)??
          ????????????????return?true;??
          ????????}??
          ????????return?false;??
          ????}??

          ????public?static?void?main(String[]?args)?{??
          ????????Person?p1?=?new?Person(1,?"aaa");??
          ????????Person?p2?=?new?Person(1,?"bbb");??
          ????????HashMap?map?=?new?HashMap<>();??
          ????????map.put(p1,?"這是p1");??
          ????????System.out.println(map.get(p2));??
          ????}??
          }

          ?原生的 equals 方法是使用 == 來比較對象的
          ?原生的 hashCode 值是根據(jù)內(nèi)存地址換算出來的一個值

          Person 類重寫 equals 方法來根據(jù) id 判斷是否相等,當(dāng)沒有重寫 hashcode 方法時,插入 p1 后便無法用 p2 取出元素,這是因為 p1 和 p2 的哈希值不相等。

          HashMap 插入元素時是根據(jù)元素的哈希值來確定存放在數(shù)組中的位置,因此HashMap 的 key 需要重寫 equals 和 hashcode 方法。

          總結(jié)

          本文描述了 HashMap 的實現(xiàn)原理,并結(jié)合源碼做了進(jìn)一步的分析,后續(xù)有空的話會聊聊有關(guān) HashMap 的線程安全問題,希望本篇文章能幫助到大家,同時也歡迎討論指正,謝謝支持!

          推薦閱讀:

          【128期】一道搜狗面試題:IO多路復(fù)用中select、poll、epoll之間的區(qū)別

          【127期】面試官:你說使用過ZooKeeper,那來說說他的基本原理吧

          【126期】消息隊列面試連環(huán)炮

          5T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機(jī),樹莓派,等等。在公眾號內(nèi)回復(fù)「2048」,即可免費獲取?。?/span>

          微信掃描二維碼,關(guān)注我的公眾號

          朕已閱?

          瀏覽 46
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人78| 欧美视频一区二区三区四区五区 | 小早川怜子爆乿护士在线观看 | 99亚州| 国产午夜精品理论 |