Java架構直通車——Java8 HashMap詳解

之前瞭解過Java併發編程實戰——併發容器之ConcurrentHashMap(JDK 1.8版本),其實已經對HashMap做了一個大致的瞭解,這裏我們來解釋一下HashMap一些相關的問題。

Java8 ConcurrentHashMap結構基本上和Java8的HashMap一樣,使用synchronized+CAS來保證線程安全性。

1. HashMap 初始大小爲何是 16?

每當插入一個元素時,我們都需要計算該值在數組中的位置,即p = tab[i = (n - 1) & hash](重點在於&運算)。

當 n = 16 時,n - 1 = 15,二進制爲 1111,這時和 hash 作與運算時,元素的位置完全取決與 hash 的大小。

倘若不是 16,如 n = 10,n - 1 = 9,二進制爲 1001,這時作與運算,很容易出現重複值,如 1101 & 1001,1011 & 1001,1111 & 1001,結果都是一樣的,所以選擇 16 以及每次擴容都乘以二的原因也可想而知了。

2. 懶加載

我們在 HashMap 的構造函數中可以發現,哈希表 Node[] table 並沒有在一開始就完成初始化;觀察 put 方法可以發現:

if ((tab = table) == null || (n = tab.length) == 0)
      n = (tab = resize()).length;

當發現哈希表爲空或者長度爲 0 時,會使用 resize 方法進行初始化,這裏很顯然運用了 lazy-load 原則,當哈希表被首次使用時,才進行初始化。

3. 樹化

Java8 中,HashMap 最大的變動就是增加了樹化處理,當鏈表中元素大於等於 8,這時有可能將鏈表改造爲紅黑樹的數據結構,爲什麼我這裏說可能呢?

final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
    int n, index; HashMap.Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //......
}

我們可以觀察樹化處理的方法 treeifyBin,發現當tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY爲 true 時,只會進行擴容處理,而沒有進行樹化;MIN_TREEIFY_CAPACITY 規定了 HashMap 可以樹化的最小表容量爲 64,這是因爲當一開始哈希表容量較小是,哈希碰撞的機率會比較大,而這個時候出現長鏈表的可能性會稍微大一些,這種原因下產生的長鏈表,我們應該優先選擇擴容而避免這類不必要的樹化。

那麼,HashMap 爲什麼要進行樹化呢?我們都知道,鏈表的查詢效率大大低於數組,而當過多的元素連成鏈表,會大大降低查詢存取的性能;同時,這也涉及到了一個安全問題,一些代碼可以利用能夠造成哈希衝突的數據對系統進行攻擊,這會導致服務端 CPU 被大量佔用。

4. 擴容resize()

擴容方法同樣是 HashMap 中十分核心的方法,同時也是比較耗性能的操作。

我們都知道數組是無法自動擴容的,所以我們需要重新計算新的容量,創建新的數組,並將所有元素拷貝到新數組中,並釋放舊數組的數據。

與以往不同的是,Java8 規定了 HashMap 每次擴容都爲之前的兩倍(n*2),也正是因爲如此,每個元素在數組中的新的索引位置只可能是兩種情況,一種爲不變,一種爲原位置 + 擴容長度(即偏移值爲擴容長度大小);

在這裏插入圖片描述

5. get(Object key)方法

final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        //檢查當前位置的第一個元素,如果正好是該元素,則直接返回
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //否則檢查是否爲樹節點,則調用 getTreeNode 方法獲取樹節點
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.TreeNode<K,V>)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;
}

主要就四步:

  1. 哈希表是否爲空或者目標位置是否存在元素
  2. 是否爲第一個元素
  3. 如果是樹節點,尋找目標樹節點
  4. 如果是鏈表結點,遍歷鏈表尋找目標結點

總結:面試時如何介紹HashMap呢?

HashMap簡單的來說,就是用的底層數組+鏈表或者紅黑樹的形式實現的。單鏈表和紅黑樹之間的轉換條件是,底層數組容量大於64的時候並且單鏈表長度大於8的時候,這樣就可以進行一個轉換。因爲在底層數組長度比較小的時候,Hash衝突會比較頻繁,更可能出現長鏈表,這時候會優先考慮擴容而不是樹化
對於擴容來說。初始時候,採用一個懶加載的方式初始化底層數組,也就是對於數組有實際操作的時候才進行初始化,這個初始化默認長度是16。對於這個數組,有負載因子默認是0.75,當哈希桶佔用量超過負載因子乘以底層數組長度的時候,就會進行一個二倍的擴容。進行二倍擴容的考慮是:對於每個元素,要麼就在當前的位置,要麼就是當前位置+擴容長度的位置,非常好計算。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章