為什麼 HashMap 的容量大小要設定為2的N次方?

壹言發表於2020-12-20

原文連結:https://www.changxuan.top/?p=1208


前兩天,我在一位同學提交中看到了下面這樣的一行程式碼,讓我很是驚訝。

Map<String, String> temp = new HashMap<>(6);

我給他說,你這樣例項化 Map 物件不好用,他不服氣。我說小朋友:如果想指定 HashMap 物件的容量得用2的N次方。他說你這也沒用。我說,我這個有用,這樣才能充分利用分配的記憶體空間。他非和我試試,我說可以,不過得先一起看看原始碼。

什麼是HashMap?

在弄懂標題的問題之前,首先需要清楚 HashMap 的概念。HashMap 是基於雜湊表的 Map 介面的實現,執行緒不安全,且不保證對映順序。

HashMap 儲存資料依賴的是陣列和[連結串列|紅黑樹],具體連結串列和紅黑樹之間如何轉換的細節此文不做詳細介紹。而本文開頭提到的例項化容量大小指的則是陣列的大小。

如何計算元素在陣列中所對應的下標?

首先計算元素的雜湊值,方法如下:

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

為什麼不直接使用 key.hashCode()的值,我們後面會提到。

計算出來雜湊值後,由於陣列容量相對來說較小肯定不能直接使用雜湊值當作索引值。所以需要使用雜湊值對陣列長度減一後的值取模。不過在在 HashMap 中可不是直接使用 % 運算子來操作的。為了提高效率,採用的是與運算的方式,程式碼如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict)
 
{
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      // n 為陣列容量, (n-1) & hash 則是計算索引值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
          ... ...
        }
}

既然清楚了計算元算在陣列中所對應下標的方法,那麼證明為什麼例項化 HashMap 物件的容量要使用2的N次方就簡單多了。

假如初始容量為2的3次方數字8,當雜湊值與容量大小減一的值進行與運算時可以保證結果比較均勻的分佈在陣列上。

  10100101 11000100 00100101
& 00000000 00000000 00000111 // 7
----------------------------------
  00000000 00000000 00000101 // 結果可以是[0,7]中的任一數字

如果初始容量為6,那麼出現雜湊衝突的機率就會增加了。

  10100101 11000100 00100101
& 00000000 00000000 00000101 // 5 
----------------------------------
  00000000 00000000 00000101 // 5
  
  10100101 11000100 00100111
& 00000000 00000000 00000101 // 5 
----------------------------------
  00000000 00000000 00000101 // 5

如果下面的值低位全是1,那麼上面的這次雜湊衝突則可以避免。那麼你想想,假如指定的容量大小為5又會怎麼樣呢?其實2的N次方數字-1的二進位制形式這個特性在好多地方會很好用,可以在小本本記上。

哦,前面說為什麼計算出來的雜湊值需要再讓高16位和低十六位做異或運算,主要是讓參與與運算的位同時具有高位和低位的特徵,來減少雜湊碰撞次數。

小朋友,還試不試啦!

相關文章