我也看源碼之初遇HashMap

我也看源碼之初遇HashMap

寫在前面
  看源碼是程序員想進一步發展的必經之路,也是面試中常被問到的知識點。而HashMap的底層相關知識點,是工作中最常使用且面試最常被問到的問題。接下來我就嘗試點進HashMap的底層去,看看它的重要方法、底層原理,學習HashMap的設計精華。當然本篇依然以JDK1.8爲基礎學習。
  首先我們來總述一下HashMap的底層,它在JDK·1.8中的底層是數組+鏈表+紅黑樹 的結構。較之前版本新增了紅黑樹,這樣做可以減少Hash衝突,那麼由鏈表轉成紅黑樹,需要滿足兩個條件,一是鏈表長度到8,二是hash桶的長度大於等於64纔會轉化紅黑樹。
  話不多說,我根據自己的理解,畫出了HashMap的底層圖,如下:

  我們把數組中的元素稱爲hash桶,在源碼中,它的定義如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> 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);
    }
    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;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

  源碼中可見每個哈希桶中包含了四個字段:hash、key、value、next,其中 next 表示鏈表的下一個節點。
  JDK 1.8 之所以添加紅黑樹是因爲一旦鏈表過長,會嚴重影響 HashMap 的性能,而紅黑樹具有快速增刪改查的特點,這樣就可以有效的解決鏈表過長時操作比較慢的問題。
  下面我們來看下源碼中的一些重要屬性。

 // HashMap 初始化長度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap 最大長度
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
// 默認的加載因子 (擴容因子)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 當鏈表長度大於此值且容量大於 64 時
static final int TREEIFY_THRESHOLD = 8;
// 轉換鏈表的臨界值,當元素小於此值時,會將紅黑樹結構轉換成鏈表結構
static final int UNTREEIFY_THRESHOLD = 6;
// 最小樹容量
static final int MIN_TREEIFY_CAPACITY = 64;

  我們知道HashMap的有一些一些重要方法如查詢、新增、擴容,下面就是他們的源碼,看一下它們是如何設計的。
首先是查詢,源碼如下:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 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 && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
       //如果第一節點是樹結構,則使用 getTreeNode 直接獲取相應的數據
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((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;
}

  上面源碼可以看出,Hash衝突時,需要判斷Key值是否相等。
  下面是新增方法

public V put(K key, V value) {
    // 對 key 進行哈希操作
    return putVal(hash(key), key, value, false, true);
}
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;
    // 根據 key 的哈希值計算出要插入的數組索引 i
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果 table[i] 等於 null,則直接插入
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果 key 已經存在了,直接覆蓋 value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果 key 不存在,判斷是否爲紅黑樹
        else if (p instanceof TreeNode)
            // 紅黑樹直接插入鍵值對
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 爲鏈表結構,循環準備插入
            for (int binCount = 0; ; ++binCount) {
                // 下一個元素爲空時
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 轉換爲紅黑樹進行處理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //  key 已經存在直接覆蓋 value
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 超過最大容量,擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

下面是擴容方法,源碼如下:

final Node<K,V>[] resize() {
    // 擴容前的數組
    Node<K,V>[] oldTab = table;
    // 擴容前的數組的大小和閾值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // 預定義新數組的大小和閾值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超過最大值就不再擴容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 擴大容量爲當前容量的兩倍,但不能超過 MAXIMUM_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 當前數組沒有數據,使用初始化的值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 如果初始化的值爲 0,則使用默認的初始化容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新的容量等於 0
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; 
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 開始擴容,將新的容量賦值給 table
    table = newTab;
    // 原數據不爲空,將原數據複製到新 table 中
    if (oldTab != null) {
        // 根據容量循環數組,複製非空元素到新 table
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                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 { // preserve order
                    // 鏈表複製,JDK 1.8 擴容優化部分
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引
                        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);
                    // 將原索引放到哈希桶中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 將原索引 + oldCap 放到哈希桶中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

  下面是幾個常見的HashMap的面試題。
  1.什麼是加載因子?加載因子爲什麼是 0.75?
  加載因子也叫擴容因子或負載因子,用來判斷什麼時候進行擴容的,假如加載因子是 0.5,HashMap 的初始化容量是 16,那麼當 HashMap 中有 16 X 0.5=8 個元素時,HashMap 就會進行擴容。
  那加載因子爲什麼是 0.75 而不是 0.5 或者 1.0 呢?這其實是出於容量和性能之間平衡的結果:
  當加載因子設置比較大的時候,擴容的門檻就被提高了,擴容發生的頻率比較低,佔用的空間會比較小,但此時發生 Hash 衝突的機率就會提升,因此需要更復雜的數據結構來存儲元素,這樣對元素的操作時間就會增加,運行效率也會因此降低;
  而當加載因子值比較小的時候,擴容的門檻會比較低,因此會佔用更多的空間,此時元素的存儲就比較稀疏,發生哈希衝突的可能性就比較小,因此操作性能會比較高。所以綜合了以上情況就取了一個 0.5 到 1.0 的平均數 0.75 作爲加載因子。
  2.爲什麼HashMap不是線程安全的?
當Hashmap在插入元素過多的時候需要進行Resize,Resize的條件是 HashMap.Size >= Capacity * LoadFactor。
Hashmap的Resize包含擴容和ReHash兩個步驟,ReHash在併發的情況下可能會形成鏈表環。當然具體得源碼中有體現。
  3.爲何HashMap的數組長度一定是2的次冪??
我認爲是有兩個原因:
  1>如果需要進行擴容,使用2的次冪會使新的數組索引位置相對以前的索引位置變化儘可能小。
如開始容量 16 ,二進制爲 1 0 0 0 0,length-1 15 二進制 爲 0 1 1 1 1
擴容後: 32, 二進制爲 1 0 0 0 0 0 ,length-1 31 ,二進制爲 0 1 1 1 1 1
  顯然擴容後只有一位的差異,如果最左位多了1,那麼再 通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位爲0,就能保證得到的新的數組索引和 老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換)
  2> 使用2的次冪會使數組的索引分佈更加均勻,length-1 的最低位總是1,任何一個位置的數變化都會導致結果不同。
ps:因爲末位的計算方法是:1*2的零次方 總是1 ,前面只要變,整個結果都會變。
再舉個例子:如果容量爲16 ,h 爲 8 或 9 ,計算的索引位置會和 容量爲15 ,h爲8 ,9 有什麼區別?
容量爲16:則 length-1=15 ,計算如下:
length-1 1 1 1 1 15 1 1 1 1 15
h 1 0 0 0 9 1 0 0 0 8
索引 0 1 1 1 7 1 0 0 0 8
容量爲15:則 length-1 =14 ,計算如下:
length-1 1 1 1 0 14 1 1 1 0 14
h 1 0 0 0 9 1 0 0 0 8
索引 1 0 0 0 8 1 0 0 0 8
  根據上面的運算 可知,容量爲奇數時很容易造成hash碰撞,碰撞後會增加鏈表,影響整體效率。
  4.HashMap和HashTable的區別?
  重要的區別有以下幾點:
  1.繼承的父類不同,HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個接口;Dictionary類是一個已經被廢棄的類,所以HashTable也是很少使用了。
  2.Hashtable既不支持Null key也不支持Null value。HashMap中,null可以作爲鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值爲null。
  3.Hashtable是線程安全的,HashMap不是線程安全的。

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