面試題:HashMap擴容機制
擴容機制
1.什麼時候才需要擴容
-
在首次呼叫put方法的時候,初始化陣列table
-
當HashMap中的元素個數超過陣列大小(陣列長度)*loadFactor(負載因子)時,就會進行陣列擴容,loadFactor的預設值(DEFAULT_LOAD_FACTOR)是0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為16,那麼當HashMap中的元素個數超過16×0.75=12(這個值就是閾值或者邊界值threshold值)的時候,就把陣列的大小擴充套件為2×16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常耗效能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預知元素的個數能夠有效的提高HashMap的效能。
-
當HashMap中的其中一個連結串列的物件個數如果達到了8個,此時如果陣列長度沒有達到64,那麼HashMap會先擴容解決,如果已經達到了64,那麼這個連結串列會變成紅黑樹,節點型別由Node變成TreeNode型別。當然,如果對映關係被移除後,下次執行resize方法時判斷樹的節點個數低於6,也會再把樹轉換為連結串列。
2.HashMap的擴容是什麼
進行擴容,會伴隨著一次重新hash分配,並且會遍歷hash表中所有的元素,是非常耗時的。在編寫程式中,要儘量避免resize。
HashMap在進行擴容時,使用的rehash方式非常巧妙,因為每次擴容都是翻倍,與原來計算的 (n-1)&hash的結果相比,只是多了一個bit位,所以節點要麼就在原來的位置,要麼就被分配到"原位置+舊容量
怎麼理解呢?例如我們從16擴充套件為32時,具體的變化如下所示:
因此元素在重新計算hash之後,因為n變為2倍,那麼n-1的標記範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
說明:5是假設計算出來的原來的索引。這樣就驗證了上述所描述的:擴容之後所以節點要麼就在原來的位置,要麼就被分配到"原位置+舊容量"這個位置。
因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就可以了,是0的話索引沒變,是1的話索引變成“原索引+oldCap(原位置+舊容量)”。可以看看下圖為16擴充為32的resize示意圖:
正是因為這樣巧妙的rehash方式,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,在resize的過程中保證了rehash之後每個桶上的節點數一定小於等於原來桶上的節點數,保證了rehash之後不會出現更嚴重的hash衝突,均勻的把之前的衝突的節點分散到新的桶中了。
3. 原始碼resize方法的解讀
下面是程式碼的具體實現:
final Node<K,V>[] resize() {
//得到當前陣列
Node<K,V>[] oldTab = table;
//如果當前陣列等於null長度返回0,否則返回當前陣列的長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//當前閥值點 預設是12(16*0.75)
int oldThr = threshold;
int newCap, newThr = 0;
//如果老的陣列長度大於0
//開始計算擴容後的大小
if (oldCap > 0) {
// 超過最大值就不再擴充了,就只好隨你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
//修改閾值為int的最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
/*
沒超過最大值,就擴充為原來的2倍
1)(newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之後容量要小於最大容量
2)oldCap >= DEFAULT_INITIAL_CAPACITY 原陣列長度大於等於陣列初始化長度16
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//閾值擴大一倍
newThr = oldThr << 1; // double threshold
}
//老閾值點大於0 直接賦值
else if (oldThr > 0) // 老閾值賦值給新的陣列長度
newCap = oldThr;
else {// 直接使用預設值
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize最大上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//新的閥值 預設原來是12 乘以2之後變為24
threshold = newThr;
//建立新的雜湊表
@SuppressWarnings({"rawtypes","unchecked"})
//newCap是新的陣列長度--》32
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//判斷舊陣列是否等於空
if (oldTab != null) {
// 把每個bucket都移動到新的buckets中
//遍歷舊的雜湊表的每個桶,重新計算桶裡元素的新位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//原來的資料賦值為null 便於GC回收
oldTab[j] = null;
//判斷陣列是否有下一個引用
if (e.next == null)
//沒有下一個引用,說明不是連結串列,當前桶上只有一個鍵值對,直接插入
newTab[e.hash & (newCap - 1)] = e;
//判斷是否是紅黑樹
else if (e instanceof TreeNode)
//說明是紅黑樹來處理衝突的,則呼叫相關方法把樹分開
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 採用連結串列處理衝突
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//通過上述講解的原理來計算節點的新位置
do {
// 原索引
next = e.next;
//這裡來判斷如果等於true e這個節點在resize之後不需要移動位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket裡
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket裡
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}