HashMap源碼分析

在看本文之前,強烈建議去讀下我的上一篇文章HashMap的hash機制詳解 ,有了這個基礎後本文才更容易理解。

在分析源碼之前,這裏對整個HashMap機制大致做下介紹,HashMap還是基於hash表的數據結構,解決hash碰撞用的也是拉鍊法,不同的是,java8不只是一個簡單的鏈表了,鏈表長度如果過長會變成紅黑樹。而且和普通固定大小的hash表不同,HashMap需要動態擴容。所以我們閱讀源碼時要重點關注:如何初始化,何時擴容,擴容時要考慮哪些,解決hash碰撞時何時變成紅黑樹,何時又會重新變成鏈表等等。

特殊屬性

首先看下HashMap有哪些關鍵屬性:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    /**
     * 數組初始化大小爲16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    /**
     *數組最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 擴容因子,如果目前數組已使用空間佔用總空間的比例達到或超過這個值就要擴容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     *鏈表的長度超過這個值就會轉換成紅黑樹
     */
    static final int TREEIFY_THRESHOLD = 8;
    /**
     *紅黑樹的節點數量小於或等於這個值就要退化到鏈表
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
 
    //真正存儲的數據結構
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    //Hash表真正存儲數據的數組
    transient Node<K,V>[] table;
   //如果總節點數量達到這個值就要擴容
    int threshold;
}

初始化

public HashMap(int initialCapacity, float loadFactor) {
       //此處省略一堆檢查校驗
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    public HashMap(int initialCapacity) {
        //初始大小默認16,擴容因子爲0.75
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        //默認0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以看到整個初始化其實就是確定兩個值,初始容量以及擴容因子,另外計算了真正需要擴容的那個閾值:threshhold
這裏大家注意,沒有對數組table進行任何實例化操作,沒有真正申請任何空間。

增加(碰撞解決)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

這裏首先是通過hash方法重新計算了key的hash值:

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

至於這個方法爲什麼這麼寫,可以參考我的上一篇文章:HashMap的hash機制詳解
然後就是真正的putVal()方法,由於方法本身比較長,這裏不一次性貼出整個方法源碼,我們一段一段來分析:

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;
        //省略其他代碼
}

這裏首先判斷了當前table數組是否爲空,空了要去開始擴容,之所以要做這個判斷是因爲 上一步初始化時,HashMap並沒有真正申請任何空間,所以這裏是利用了擴容方法resize()順帶初始化table數組了。

 */
    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 ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
       //省略代碼
}

這裏大家注意i=(n-1)&hash的計算,這個就是根據hash值來確定當前value放置在table數組的哪個位置上,爲什麼這麼算還是參考我的上篇文章:HashMap的hash機制詳解。先判斷目標位置有沒有被之前的元素佔用,如果沒有直接放到該位置上,否者就是下面要開始處理hash碰撞了。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //此處省略代碼
        else {
            Node<K,V> e; K k;
            //此處的p就是產生hash碰撞位置的原有的數據,e用來存放被覆蓋的數據
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))   // 1
                e = p;    
            else if (p instanceof TreeNode)   // 2
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {   //  3
                //此處開始遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null); //4
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st    
                            //5
                            treeifyBin(tab, hash);
                        break;
                    }
                  
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))   //6
                        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;
            } 
        }
       //此處省略代碼

hash碰撞頁分成好幾種情況,

  • key完全相同 ,這就是1號if對應的情況,key完全相同時,這裏直接記錄了原來的值,這裏並沒有直接覆蓋
  • key不同,但hash值相同,而且由於此處hash碰撞次數太多,處理碰撞的鏈表已經變成了紅黑樹,這是2號if對應的情況,這裏直接將新的值插入到紅黑樹即可
  • key不同,但hash值相同,而且目前處理hash碰撞還是鏈表形態,這裏對應3好else情況,如果順利的話直接遍歷鏈表到尾節點,然後插入新的值(情況4),插入新值後如果鏈表長度達到了TREEIFY_THRESHOLD(8)就會開始轉變成紅黑樹(情況5),以上是理想情況,但還有一種情況,如果和之前鏈表中某個節點的key相同呢(情況6)? 這個時候就不用繼續遍歷鏈表了,此時的e就指向了那個相同key的節點。

上面我們處理了hash碰撞中key不相同的情況,現在看看key相同怎麼處理的:

            //此處的e就是key相同時的舊值,會被新的value替代
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

可以看到沒有什麼操作,只是在允許(HashMap可以設置不覆蓋同key的舊值)的情況下直接覆蓋舊值,並返回新值。
現在插入完成了,我們還需要判斷是否需要擴容:

        if (++size > threshold)
            resize();

這裏注意下,如果是同key的話,hashmap並沒有使用新的空間,只是覆蓋而已,上面已經直接return oldValue了,只有在插入了新的值的時候纔會判斷擴容。

擴容

在分析源碼之前,大家先思考下:擴容到底擴的是哪部分?擴容時如果是table+鏈表的結構,擴容後鏈表怎麼辦?如果是紅黑樹呢?紅黑樹怎麼辦?

確定要擴多大

        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {   // 1
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) //  2   initial capacity was placed in threshold
            newCap = oldThr;
        else {               // 3     zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;

情況2是初始化時指定了threshold,這個情況比較少,我們重點關注情況3和情況1。

  • 第一次擴容(情況3)
    之前初始化時我們還記得那時只是簡單的確定了初始容量和擴容因子,具體空間沒有申請,我們第一次put的時候就會觸發這個擴容機制,進入到情況3中來。
  • 後續擴容(情況1)
    後續正常擴容一般都會進入情況1,超過最大MAXIMUM_CAPAXITY的情況也比較少,所以一般都會走
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;

這樣容量翻倍,擴容threshold也翻倍。

申請新空間

@SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

這一步沒什麼,直接new了一個新的數組

原有數據遷移

接下來我們要將原來數組中的數據遷移到新的數組中來

       for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = 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);

這裏對於紅黑樹的部分比較負責,本文就不做講解,有興趣的可以自己研究

  • 碰撞後產生鏈表的元素
    在看源碼之前,我們先考慮一個問題:擴容後所有元素的位置都需要重新計算嗎?我們舉個例子說明:
    原有容量爲16,有兩個元素hash值分別爲 e1: 2, e2:18,
    2 &(16-1) = 2;
    18 &(16-1) = 2;
    這兩個元素都放置在位置2上,現在擴容一倍,兩個元素如果重新計算位置的話
    2 & (32 -1) = 2;
    18 & (32 -1) = 18;
    可以看到e1的位置還是不變的,e2的位置雖然變了,但只是在原有位置上增加了16而已,有了這個規律,我們就可以將鏈表上的元素分爲兩種,一種在新數組位置不變newTab[j]上,一種在新數組 newTab[j+oldCap]上。接下來我們再看源碼:
else { // preserve order
                        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;
                            }
                            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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }

理解上面說的再看源碼就很好理解了,就是將原本一條鏈表拆成了兩條,一條放置在newTab[j]上,一條放置在newTab[j+oldCap]上。至於如何判斷一個元素是否是在原位置還是在新位置上,這裏用了一個位運算:

if ((e.hash & oldCap) == 0) {

如果等於0則在原位置,否則就要放置在新位置。

總結

到這裏,hashmap基本就分析的差不多了,剩下的get,remove,update之類的操作也就沒有必要再多說了,有了上面的基礎一看就明白了,希望這篇文章對大家有幫助,如果有理解不到位的,也請各位指正。

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