前言
Java中使用最多的資料結構基本就是ArrayList和HashMap,HashMap的原理也常常出現在各種面試題中,本文就HashMap的設計與設計緣由作出一一講解,並解答面試常見的一些問題。
一 HashMap資料結構
HashMap是一張雜湊表(即陣列),表中的每個元素都是鍵值對(Map.Entry類)。並且每個元素都是一個連結串列(紅黑樹)的節點。並且HashMap的陣列長度一定是2的次冪。
1.1 為何陣列長度一定是2的次冪
正常情況下,新增節點時,會對節點進行取模運算,確定節點在雜湊表中的位置。但是當雜湊表(陣列)長度為2的次冪時,取模運算可以修改為位與運算。
原始碼如下:
static final int hash(Object key) {
if (key == null){
return 0;
}
int h;
h = key.hashCode();返回雜湊值也就是hashcode
// ^ :按位異或
// >>>:無符號右移,忽略符號位,空位都以0補齊
//其中n是陣列的長度,即Map的陣列部分初始化長度
return (n-1)&(h ^ (h >>> 16));
}
具體原理可以參考專門講解該演算法的文章:
由HashMap雜湊演算法引出的求餘%和與運算&轉換問題
二 HashMap的鍵值儲存
我們給 put() 方法傳遞鍵和值時,我們先對鍵呼叫 hashCode() 方法,計算並返回 hashCode,然後使用HashMap內部的hash演算法,將hashCode計算為表中的具體位置,找到 Map 陣列的 bucket 位置來儲存 Node 物件。
三 解決Hash碰撞
使用拉鍊法
如果hash到的陣列位置已存在物件,即為Hash碰撞。JDK使用拉鍊法解決Hash碰撞問題。
即以原有的Node節點為基礎,構造連結串列。將新的Node節點設為連結串列表頭。
3.1 為何新節點為表頭
如果已原有節點為表頭,則需要遍歷連結串列,徒增不必要的效能消耗
3.2 連結串列過長導致的複雜度問題
HashMap的查詢操作最佳時間複雜度是O(1)
,但是當表中的某個連結串列過長時,查詢該連結串列上的元素時間複雜度為O(n)
。JDK1.8
中解決了該問題,當HashMap中某連結串列長度大於8時,連結串列會重構為紅黑樹,這樣,HashMap的最壞時間複雜度為O(n)
。同理,為了不必要的消耗,當連結串列長度小於6時,紅黑樹會重新變回連結串列
3.3 還有什麼方法解決Hash碰撞
開放定址法,再雜湊法
感興趣可以參看此文:
Hash碰撞和解決策略
四 HashMap的擴容
4.1 擴容時機
當size超過閾值(**陣列長度*負載因子**)時,即開始擴容,HashMap的負載因子為0.75。
4.1.1 為何要陣列未滿就擴容
避免頻繁出現Hash碰撞,造成拉鍊過長(紅黑樹過長)。這樣會導致查詢複雜度頻繁出現最壞情況
4.2 擴容過程
建立原本陣列容量*2的新陣列,將節點從原本的陣列中遷移過去。
4.2.1 為何擴容的倍數是2倍
原因一上文已說明,方便進行雜湊運算。
原因二是不需要重新計算Hash值(JDK1.8
優化)。經過觀測可以發現,我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,經過rehash之後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。對應的就是下方的resize的註釋。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() { }
看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容後key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的雜湊值(也就是根據key1算出來的hashcode值)與高位與運算的結果。
元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。
五 重寫equals方法需同時重寫hashCode方法
這個是老生常談的問題了,如果順利理解了HashMap的底層結構那麼這個問題就很好理解了。equals相同的key理論上必定有相同hashCode,所以必須也重寫hashCode方法。可以思考下如果沒重寫,在put,get過程中會導致什麼問題。