面試再問(wèn) HashMap,求你把這篇文章發(fā)給他!
點(diǎn)擊上方藍(lán)色“小哈學(xué)Java”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!


來(lái)源:sf.gg/a/1190000022184751
數(shù)據(jù)結(jié)構(gòu) table數(shù)組長(zhǎng)度永遠(yuǎn)為2的冪次方 擴(kuò)容 查找 插入 刪除 遍歷 equasl和hashcode 總結(jié)
HashMap是面試中經(jīng)常問(wèn)到的一個(gè)知識(shí)點(diǎn),也是判斷一個(gè)候選人基礎(chǔ)是否扎實(shí)的標(biāo)準(zhǔn)之一,因?yàn)橥ㄟ^(guò)HashMap可以引出很多知識(shí)點(diǎn),比如數(shù)據(jù)結(jié)構(gòu)(數(shù)組、鏈表、紅黑樹(shù))、equals和hashcode方法,除此之外還可以引出線程安全的問(wèn)題,HashMap是我在初學(xué)階段學(xué)到的設(shè)計(jì)的最為巧妙的集合,里面有很多細(xì)節(jié)以及優(yōu)化技巧都值得我們深入學(xué)習(xí),本文將會(huì)涉及到以下問(wèn)題
默認(rèn)大小、負(fù)載因子以及擴(kuò)容倍數(shù) 底層數(shù)據(jù)結(jié)構(gòu) 如何處理hash沖突 如何計(jì)算key的hash值 數(shù)組長(zhǎng)度為什么是2的冪次方 查找、插入、擴(kuò)容過(guò)程 fail-fast機(jī)制
如果上面的都能回答出來(lái)的話那么這篇文章可能不太適合你,話不多說(shuō)進(jìn)入正文。注意:本文源碼都是以JDK1.8版本講解
數(shù)據(jù)結(jié)構(gòu)
在 JDK1.8 中,HashMap 是由 數(shù)組+鏈表+紅黑樹(shù)構(gòu)成(1.7版本是數(shù)組+鏈表)當(dāng)一個(gè)值中要存儲(chǔ)到HashMap中的時(shí)候會(huì)根據(jù)Key的值來(lái)計(jì)算出他的hash,通過(guò)hash值來(lái)確認(rèn)存放到數(shù)組中的位置,如果發(fā)生hash沖突就以鏈表的形式存儲(chǔ),當(dāng)鏈表過(guò)長(zhǎng)的話,HashMap會(huì)把這個(gè)鏈表轉(zhuǎn)換成紅黑樹(shù)來(lái)存儲(chǔ),如圖所示:

在看源碼之前我們需要先看看一些基本屬性
//默認(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;
//元素個(gè)數(shù)
transient?int?size;
//容量閾值(元素個(gè)數(shù)大于等于該值時(shí)會(huì)自動(dòng)擴(kuò)容)
int?threshold;
table數(shù)組里面存放的是Node對(duì)象,Node是HashMap的一個(gè)內(nèi)部類(lèi),用來(lái)表示一個(gè)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.75threshold = 數(shù)組長(zhǎng)度 * loadFactor,當(dāng)元素個(gè)數(shù)大于等于threshold(容量閾值)時(shí),HashMap會(huì)進(jìn)行擴(kuò)容操作table數(shù)組中存放指向鏈表的引用
這里需要注意的一點(diǎn)是table數(shù)組并不是在構(gòu)造方法里面初始化的,它是在resize(擴(kuò)容)方法里進(jìn)行初始化的。
這里說(shuō)句題外話:可能有刁鉆的面試官會(huì)問(wèn)為什么默認(rèn)初始容量要設(shè)置為16?為什么負(fù)載因子要設(shè)置為0.75?我們都知道HashMap數(shù)組長(zhǎng)度被設(shè)計(jì)成2的冪次方(下面會(huì)講),那為什么初始容量不設(shè)計(jì)成4、8或者32.... 其實(shí)這是JDK設(shè)計(jì)者經(jīng)過(guò)權(quán)衡之后得出的一個(gè)比較合理的數(shù)字,,如果默認(rèn)容量是8的話,當(dāng)添加到第6個(gè)元素的時(shí)候就會(huì)觸發(fā)擴(kuò)容操作,擴(kuò)容操作是非常消耗CPU的,32的話如果只添加少量元素則會(huì)浪費(fèi)內(nèi)存,因此設(shè)計(jì)成16是比較合適的,負(fù)載因子也是同理。
table數(shù)組長(zhǎng)度永遠(yuǎn)為2的冪次方
總所周知,HashMap數(shù)組長(zhǎng)度永遠(yuǎn)為2的冪次方(指的是table數(shù)組的大小),那你有想過(guò)為什么嗎?
首先我們需要知道HashMap是通過(guò)一個(gè)名為tableSizeFor的方法來(lái)確保HashMap數(shù)組長(zhǎng)度永遠(yuǎn)為2的冪次方的,源碼如下:
/*找到大于或等于?cap?的最小2的冪,用來(lái)做容量閾值*/
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)?。最后再讓結(jié)果n+1,即得到了2的整數(shù)次冪的值了。
讓cap-1再賦值給n的目的是另找到的目標(biāo)值大于或等于原值。例如二進(jìn)制1000,十進(jìn)制數(shù)值為8。如果不對(duì)它減1而直接操作,將得到答案10000,即16。顯然不是結(jié)果。減1后二進(jìn)制為111,再進(jìn)行操作則會(huì)得到原來(lái)的數(shù)值1000,即8。通過(guò)一系列位運(yùn)算大大提高效率。
“那在什么地方會(huì)用到
tableSizeFor方法呢?
答案就是在構(gòu)造方法里面調(diào)用該方法來(lái)設(shè)置threshold,也就是容量閾值。
“這里你可能又會(huì)有一個(gè)疑問(wèn):為什么要設(shè)置為threshold呢?
因?yàn)樵跀U(kuò)容方法里第一次初始化table數(shù)組時(shí)會(huì)將threshold設(shè)置數(shù)組的長(zhǎng)度,后續(xù)在講擴(kuò)容方法時(shí)再介紹。
/*傳入初始容量和負(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ù)組長(zhǎng)度設(shè)計(jì)為2的冪次方呢?
我個(gè)人覺(jué)得這樣設(shè)計(jì)有以下幾個(gè)好處:
“1、當(dāng)數(shù)組長(zhǎng)度為2的冪次方時(shí),可以使用位運(yùn)算來(lái)計(jì)算元素在數(shù)組中的下標(biāo)
HashMap是通過(guò)index=hash&(table.length-1)這條公式來(lái)計(jì)算元素在table數(shù)組中存放的下標(biāo),就是把元素的hash值和數(shù)組長(zhǎng)度減1的值做一個(gè)與運(yùn)算,即可求出該元素在數(shù)組中的下標(biāo),這條公式其實(shí)等價(jià)于hash%length,也就是對(duì)數(shù)組長(zhǎng)度求模取余,只不過(guò)只有當(dāng)數(shù)組長(zhǎng)度為2的冪次方時(shí),hash&(length-1)才等價(jià)于hash%length,使用位運(yùn)算可以提高效率。
“2、 增加hash值的隨機(jī)性,減少hash沖突
如果 length 為 2 的冪次方,則 length-1 轉(zhuǎn)化為二進(jìn)制必定是 11111……的形式,這樣的話可以使所有位置都能和元素hash值做與運(yùn)算,如果是如果 length 不是2的次冪,比如length為15,則length-1為14,對(duì)應(yīng)的二進(jìn)制為1110,在和hash 做與運(yùn)算時(shí),最后一位永遠(yuǎn)都為0 ,浪費(fèi)空間。
擴(kuò)容
HashMap每次擴(kuò)容都是建立一個(gè)新的table數(shù)組,長(zhǎng)度和容量閾值都變?yōu)樵瓉?lái)的兩倍,然后把原數(shù)組元素重新映射到新數(shù)組上,具體步驟如下:
首先會(huì)判斷table數(shù)組長(zhǎng)度,如果大于0說(shuō)明已被初始化過(guò),那么 按當(dāng)前table數(shù)組長(zhǎng)度的2倍進(jìn)行擴(kuò)容,閾值也變?yōu)樵瓉?lái)的2倍若table數(shù)組未被初始化過(guò),且threshold(閾值)大于0說(shuō)明調(diào)用了 HashMap(initialCapacity, loadFactor)構(gòu)造方法,那么就把數(shù)組大小設(shè)為threshold若table數(shù)組未被初始化,且threshold為0說(shuō)明調(diào)用 HashMap()構(gòu)造方法,那么就把數(shù)組大小設(shè)為16,threshold設(shè)為16*0.75接著需要判斷如果不是第一次初始化,那么擴(kuò)容之后,要重新計(jì)算鍵值對(duì)的位置,并把它們移動(dòng)到合適的位置上去,如果節(jié)點(diǎn)是紅黑樹(shù)類(lèi)型的話則需要進(jìn)行紅黑樹(shù)的拆分。
這里有一個(gè)需要注意的點(diǎn)就是在JDK1.8 HashMap擴(kuò)容階段重新映射元素時(shí)不需要像1.7版本那樣重新去一個(gè)個(gè)計(jì)算元素的hash值,而是通過(guò)hash & oldCap的值來(lái)判斷,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長(zhǎng)度,為什么呢?具體原因如下:
“因?yàn)槲覀兪褂玫氖?次冪的擴(kuò)展(指長(zhǎng)度擴(kuò)為原來(lái)2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動(dòng)2次冪的位置。因此,我們?cè)跀U(kuò)充HashMap的時(shí)候,不需要像JDK1.7的實(shí)現(xiàn)那樣重新計(jì)算hash,只需要看看原來(lái)的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒(méi)變,是1的話索引變成“原索引+oldCap

這點(diǎn)其實(shí)也可以看做長(zhǎng)度為2的冪次方的一個(gè)好處,也是HashMap 1.7和1.8之間的一個(gè)區(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?說(shuō)明hash數(shù)組table已被初始化
????if?(oldCap?>?0)?{
????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
????????????threshold?=?Integer.MAX_VALUE;
????????????return?oldTab;
????????}//按當(dāng)前table數(shù)組長(zhǎng)度的2倍進(jìn)行擴(kuò)容,閾值也變?yōu)樵瓉?lái)的2倍
????????else?if?((newCap?=?oldCap?<1)?=?DEFAULT_INITIAL_CAPACITY)
????????????newThr?=?oldThr?<1;
????}//2、若數(shù)組未被初始化,而threshold>0說(shuō)明調(diào)用了HashMap(initialCapacity)和HashMap(initialCapacity,?loadFactor)構(gòu)造器
????else?if?(oldThr?>?0)
????????newCap?=?oldThr;//新容量設(shè)為數(shù)組閾值
????else?{?//3、若table數(shù)組未被初始化,且threshold為0說(shuō)明調(diào)用HashMap()構(gòu)造方法
????????newCap?=?DEFAULT_INITIAL_CAPACITY;//默認(rèn)為16
????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);//16*0.75
????}
????//若計(jì)算過(guò)程中,閾值溢出歸零,則按閾值公式重新計(jì)算
????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)//如果只鏈接一個(gè)節(jié)點(diǎn),重新計(jì)算并放入新數(shù)組
????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
????????????????//若是紅黑樹(shù),則需要進(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ù)組長(zhǎng)度*/
????????????????????????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)紅黑樹(shù)的幾個(gè)知識(shí)點(diǎn):
鏈表樹(shù)化
指的就是把鏈表轉(zhuǎn)換成紅黑樹(shù),樹(shù)化需要滿(mǎn)足以下兩個(gè)條件:
鏈表長(zhǎng)度大于等于8 table數(shù)組長(zhǎng)度大于等于64
為什么table數(shù)組容量大于等于64才樹(shù)化?
因?yàn)楫?dāng)table數(shù)組容量比較小時(shí),鍵值對(duì)節(jié)點(diǎn) hash 的碰撞率可能會(huì)比較高,進(jìn)而導(dǎo)致鏈表長(zhǎng)度較長(zhǎng)。這個(gè)時(shí)候應(yīng)該優(yōu)先擴(kuò)容,而不是立馬樹(shù)化。
紅黑樹(shù)拆分
拆分就是指擴(kuò)容后對(duì)元素重新映射時(shí),紅黑樹(shù)可能會(huì)被拆分成兩條鏈表。
由于篇幅有限,有關(guān)紅黑樹(shù)這里就不展開(kāi)了。
查找
在看源碼之前先來(lái)簡(jiǎn)單梳理一下查找流程:
首先通過(guò)自定義的hash方法計(jì)算出key的hash值,求出在數(shù)組中的位置 判斷該位置上是否有節(jié)點(diǎn),若沒(méi)有則返回null,代表查詢(xún)不到指定的元素 若有則判斷該節(jié)點(diǎn)是不是要查找的元素,若是則返回該節(jié)點(diǎn) 若不是則判斷節(jié)點(diǎn)的類(lèi)型,如果是紅黑樹(shù)的話,則調(diào)用紅黑樹(shù)的方法去查找元素 如果是鏈表類(lèi)型,則遍歷鏈表調(diào)用equals方法去查找元素
HashMap的查找是非??斓?,要查找一個(gè)元素首先得知道key的hash值,在HashMap中并不是直接通過(guò)key的hashcode方法獲取哈希值,而是通過(guò)內(nèi)部自定義的hash方法計(jì)算哈希值,我們來(lái)看看其實(shí)現(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ù)參與到計(jì)算中,int有 32 位,右移16位就能讓低16位和高16位進(jìn)行異或,也是為了增加hash值的隨機(jī)性。
知道如何計(jì)算hash值后我們來(lái)看看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ù)組鏈接的第一個(gè)節(jié)點(diǎn),e指向下一個(gè)節(jié)點(diǎn)
????int?n;//hash數(shù)組長(zhǎng)度
????K?k;
????/*(n?-?1)?&?hash?————>根據(jù)hash值計(jì)算出在數(shù)組中的索引index(相當(dāng)于對(duì)數(shù)組長(zhǎng)度取模,這里用位運(yùn)算進(jìn)行了優(yōu)化)*/
????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(first?=?tab[(n?-?1)?&?hash])?!=?null)?{
????????//基本類(lèi)型用==比較,其它用equals比較
????????if?(first.hash?==?hash?&&?((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????return?first;
????????if?((e?=?first.next)?!=?null)?{
????????????//如果first是TreeNode類(lèi)型,則調(diào)用紅黑樹(shù)查找方法
????????????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;
}
這里要注意的一點(diǎn)就是在HashMap中用 (n - 1) & hash 計(jì)算key所對(duì)應(yīng)的索引index(相當(dāng)于對(duì)數(shù)組長(zhǎng)度取模,這里用位運(yùn)算進(jìn)行了優(yōu)化),這點(diǎn)在上面已經(jīng)說(shuō)過(guò)了,就不再?gòu)U話了。
插入
我們先來(lái)看看插入元素的步驟:
當(dāng)table數(shù)組為空時(shí),通過(guò)擴(kuò)容的方式初始化table 通過(guò)計(jì)算鍵的hash值求出下標(biāo)后,若該位置上沒(méi)有元素(沒(méi)有發(fā)生hash沖突),則新建Node節(jié)點(diǎn)插入 若發(fā)生了hash沖突,遍歷鏈表查找要插入的key是否已經(jīng)存在,存在的話根據(jù)條件判斷是否用新值替換舊值 如果不存在,則將元素插入鏈表尾部,并根據(jù)鏈表長(zhǎng)度決定是否將鏈表轉(zhuǎn)為紅黑樹(shù) 判斷鍵值對(duì)數(shù)量是否大于等于閾值,如果是的話則進(jìn)行擴(kuò)容操作
先看完上面的流程,再來(lái)看源碼會(huì)簡(jiǎn)單很多,源碼如下:
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中第一個(gè)節(jié)點(diǎn)
????int?n,?i;//n為數(shù)組長(zhǎng)度,i為索引
????//tab被延遲到插入新數(shù)據(jù)時(shí)再進(jìn)行初始化
????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????????n?=?(tab?=?resize()).length;
????//如果數(shù)組中不包含Node引用,則新建Node節(jié)點(diǎn)存入數(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é)點(diǎn)
????????K?k;
????????//如果第一個(gè)節(jié)點(diǎn)就是要插入的key-value,則讓e指向第一個(gè)節(jié)點(diǎn)(p在這里指向第一個(gè)節(jié)點(diǎn))
????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????e?=?p;
????????//如果p是TreeNode類(lèi)型,則調(diào)用紅黑樹(shù)的插入操作(注意:TreeNode是Node的子類(lèi))
????????else?if?(p?instanceof?TreeNode)
????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);
????????else?{
????????????//對(duì)鏈表進(jìn)行遍歷,并用binCount統(tǒng)計(jì)鏈表長(zhǎng)度
????????????for?(int?binCount?=?0;?;?++binCount)?{
????????????????//如果鏈表中不包含要插入的key-value,則將其插入到鏈表尾部
????????????????if?((e?=?p.next)?==?null)?{
????????????????????p.next?=?newNode(hash,?key,?value,?null);
????????????????????//如果鏈表長(zhǎng)度大于或等于樹(shù)化閾值,則進(jìn)行樹(shù)化操作
????????????????????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說(shuō)明要插入的key-value已存在
????????if?(e?!=?null)?{
????????????V?oldValue?=?e.value;
????????????//根據(jù)傳入的onlyIfAbsent判斷是否要更新舊值
????????????if?(!onlyIfAbsent?||?oldValue?==?null)
????????????????e.value?=?value;
????????????afterNodeAccess(e);
????????????return?oldValue;
????????}
????}
????++modCount;
????//鍵值對(duì)數(shù)量大于等于閾值時(shí),則進(jìn)行擴(kuò)容
????if?(++size?>?threshold)
????????resize();
????afterNodeInsertion(evict);//也是空函數(shù)?回調(diào)?不知道干嘛的
????return?null;
}
從源碼也可以看出table數(shù)組是在第一次調(diào)用put方法后才進(jìn)行初始化的。這里還有一個(gè)知識(shí)點(diǎn)就是在JDK1.8版本HashMap是在鏈表尾部插入元素的,而在1.7版本里是插入鏈表頭部的,1.7版本這么設(shè)計(jì)的原因可能是作者認(rèn)為新插入的元素使用到的頻率會(huì)比較高,插入頭部的話可以減少遍歷次數(shù)。
那為什么1.8改成尾插法了呢?主要是因?yàn)轭^插法在多線程環(huán)境下可能會(huì)導(dǎo)致兩個(gè)節(jié)點(diǎn)互相引用,形成死循環(huán),由于此文主要講解1.8版本,感興趣的小伙伴可以去看看1.7版本的源碼。
刪除
HashMap的刪除操作并不復(fù)雜,僅需三個(gè)步驟即可完成。
定位桶位置 遍歷鏈表找到相等的節(jié)點(diǎn) 第三步刪除節(jié)點(diǎn)
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;
????????//?如果鍵的值與鏈表第一個(gè)節(jié)點(diǎn)相等,則將?node?指向該節(jié)點(diǎn)
????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????node?=?p;
????????else?if?((e?=?p.next)?!=?null)?{
????????????//?如果是?TreeNode?類(lèi)型,調(diào)用紅黑樹(shù)的查找邏輯定位待刪除節(jié)點(diǎn)
????????????if?(p?instanceof?TreeNode)
????????????????node?=?((TreeNode)p).getTreeNode(hash,?key);
????????????else?{
????????????????//?2、遍歷鏈表,找到待刪除節(jié)點(diǎn)
????????????????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é)點(diǎn),并修復(fù)鏈表或紅黑樹(shù)
????????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é)點(diǎn)后可能破壞了紅黑樹(shù)的平衡性質(zhì),removeTreeNode方法會(huì)對(duì)紅黑樹(shù)進(jìn)行變色、旋轉(zhuǎn)等操作來(lái)保持紅黑樹(shù)的平衡結(jié)構(gòu),這部分比較復(fù)雜,感興趣的小伙伴可看下面這篇文章:紅黑樹(shù)詳解
遍歷
在工作中HashMap的遍歷操作也是非常常用的,也許有很多小伙伴喜歡用for-each來(lái)遍歷,但是你知道其中有哪些坑嗎?
看下面的例子,當(dāng)我們?cè)诒闅vHashMap的時(shí)候,若使用remove方法刪除元素時(shí)會(huì)拋出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");
????????}
這就是常說(shuō)的fail-fast(快速失敗)機(jī)制,這個(gè)就需要從一個(gè)變量說(shuō)起
transient?int?modCount;
在HashMap中有一個(gè)名為modCount的變量,它用來(lái)表示集合被修改的次數(shù),修改指的是插入元素或刪除元素,可以回去看看上面插入刪除的源碼,在最后都會(huì)對(duì)modCount進(jìn)行自增。
當(dāng)我們?cè)诒闅vHashMap時(shí),每次遍歷下一個(gè)元素前都會(huì)對(duì)modCount進(jìn)行判斷,若和原來(lái)的不一致說(shuō)明集合結(jié)果被修改過(guò)了,然后就會(huì)拋出異常,這是Java集合的一個(gè)特性,我們這里以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迭代器基類(lèi),子類(lèi)有KeyIterator、ValueIterator等*/
abstract?class?HashIterator?{
????Node?next;????????//下一個(gè)節(jié)點(diǎn)
????Node?current;?????//當(dāng)前節(jié)點(diǎn)
????int?expectedModCount;??//修改次數(shù)
????int?index;?????????????//當(dāng)前索引
????//無(wú)參構(gòu)造
????HashIterator()?{
????????expectedModCount?=?modCount;
????????Node[]?t?=?table;
????????current?=?next?=?null;
????????index?=?0;
????????//找到第一個(gè)不為空的桶的索引
????????if?(t?!=?null?&&?size?>?0)?{
????????????do?{}?while?(index?null);
????????}
????}
????//是否有下一個(gè)節(jié)點(diǎn)
????public?final?boolean?hasNext()?{
????????return?next?!=?null;
????}
????//返回下一個(gè)節(jié)點(diǎn)
????final?Node?nextNode()? {
????????Node[]?t;
????????Node?e?=?next;
????????if?(modCount?!=?expectedModCount)
????????????throw?new?ConcurrentModificationException();//fail-fast
????????if?(e?==?null)
????????????throw?new?NoSuchElementException();
????????//當(dāng)前的鏈表遍歷完了就開(kāi)始遍歷下一個(gè)鏈表
????????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被修改了則會(huì)拋出ConcurrentModificationException異常。
if?(modCount?!=?expectedModCount)
????????????throw?new?ConcurrentModificationException();
那么如何在遍歷時(shí)刪除元素呢?
我們可以看看迭代器自帶的remove方法,其中最后兩行代碼如下:
removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode
expectedModCount?=?modCount;
意思就是會(huì)調(diào)用外部remove方法刪除元素后,把modCount賦值給expectedModCount,這樣的話兩者一致就不會(huì)拋出異常了,所以我們應(yīng)該這樣寫(xiě):
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();
????????}
這里還有一個(gè)知識(shí)點(diǎn)就是在遍歷HashMap時(shí),我們會(huì)發(fā)現(xiàn)遍歷的順序和插入的順序不一致,這是為什么?
在HashIterator源碼里面可以看出,它是先從桶數(shù)組中找到包含鏈表節(jié)點(diǎn)引用的桶。然后對(duì)這個(gè)桶指向的鏈表進(jìn)行遍歷。遍歷完成后,再繼續(xù)尋找下一個(gè)包含鏈表節(jié)點(diǎn)引用的桶,找到繼續(xù)遍歷。找不到,則結(jié)束遍歷。這就解釋了為什么遍歷和插入的順序不一致,不懂的同學(xué)請(qǐng)看下圖:

equasl和hashcode
我在面試中就被問(wèn)到過(guò)HashMap的key有什么限制嗎?相信很多人都知道HashMap的key需要重寫(xiě)equals和hashcode方法。
為什么HashMap的key需要重寫(xiě)equals()和hashcode()方法?
簡(jiǎn)單看個(gè)例子,這里以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方法是使用==來(lái)比較對(duì)象的 原生的hashCode值是根據(jù)內(nèi)存地址換算出來(lái)的一個(gè)值
Person類(lèi)重寫(xiě)equals方法來(lái)根據(jù)id判斷是否相等,當(dāng)沒(méi)有重寫(xiě)hashcode方法時(shí),插入p1后便無(wú)法用p2取出元素,這是因?yàn)閜1和p2的哈希值不相等。
HashMap插入元素時(shí)是根據(jù)元素的哈希值來(lái)確定存放在數(shù)組中的位置,因此HashMap的key需要重寫(xiě)equals和hashcode方法。
總結(jié)
本文描述了HashMap的實(shí)現(xiàn)原理,并結(jié)合源碼做了進(jìn)一步的分析,其實(shí)還有很多相關(guān)的知識(shí)點(diǎn)沒(méi)有講到,比如HashMap的線程安全問(wèn)題、1.7和1.8版本之間的區(qū)別....后續(xù)如果有時(shí)間的話會(huì)繼續(xù)寫(xiě)文章和大家交流交流,另外博主也針對(duì)ConcurrentHashMap寫(xiě)了一篇文章,想了解的同學(xué)可以去看看一文看懂ConcurrentHashMap,希望本篇文章能幫助到大家,同時(shí)也歡迎討論指正,謝謝支持!
END
有熱門(mén)推薦?
1.?問(wèn)號(hào)臉:為什么 Java 中 “1000==1000” 為 false,而 ”100==100“ 為 true?
2.?原來(lái) Elasticsearch 還可以這么理解
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

