HashMap(1.8)源碼分析

 HashMap是我們日常開發中使用非常頻繁的集合框架,平時只會使用,卻不知道底層是如何實現的,今天在這裏對HashMap的底層實現做一系列的分析。如有不對的地方,歡迎指正。

我們知道HashMap是用來存儲數據的一個容器,使用key-value來存儲數據,當key重複的時會覆蓋前一次的value值。那麼HashMap是如何做到存儲數據和讀取數據的呢?我們知道1.8之後的HashMap底層使用了數組、鏈表+紅黑樹的數據結構,那麼具體的實現是什麼樣的呢?讓我一步一步介紹。

首先,HashMap是在什麼時候初始化的,是new HashMap()的時候嗎?我們來看一下源碼:

我們看到它又四個構造方法,這四個構造方法沒有對HashMap做初始化的操作(具體的代碼請自行查找,這裏就不貼出來了),而點到put("key","value");的方法裏它繼續調用了一個putVal(hash(key), key, value, false, true);方法,然後繼續跟進,在該方法裏有一個resize();方法,這個方法部分源碼如下:

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;
            }
            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
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            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
                        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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

我們具體關注兩句代碼,

1:newCap = DEFAULT_INITIAL_CAPACITY;

2:Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

我們先看一下DEFAULT_INITIAL_CAPACITY是什麼:

上圖可以見,就是一個int類型的變量,賦值16,然後第2句代碼就很明顯了,創建了一個長度爲16的Node數組,那麼Node又是什麼呢?我們繼續跟進去看一下:

 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;
        }
    ...略
 }

上述可知,Node就是一個對象,有四個屬性,分別是hash值,key值,value和next,這就很明顯了,HashMap底層就是用這個Node對象來存儲數據的,hash值用作標識,next指向下一個對象,key和value就不用多說了。那麼這個hash是誰的hash呢,我們根據Node的構造方法往回找,找到一個putMapEntries(Map<? extends K, ? extends V> m, boolean evict)方法,我們來看一下:

 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

在最後一句,我們終於找到hash是誰的了,那就是我們put值的時候的key的Hash,跟進hash(key),得到

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

也就是key調用看hashCode()方法,因爲所有對象都是Object的子類,所以這個key可以根據父類的hashCode()方法獲得哈希值。

我們知道,hashCode值就是一串的數字如:106079,說白了,hashCode是用來定位的,我們前面初始化了一個長度爲16的數字,那麼我們在插入元素的時候就需要知道下標,那麼如果是十進制的哈希值,定位這16個位置,我們可以通過取餘,但是這樣做的話衝突頻率太高了,於是乎,這裏將十進制的哈希值轉換爲了二進制,我們可以看到上面的這句代碼:return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);如果key爲null那麼哈希值直接返回0,否則,將哈希值的低16位與哈希值的高16位做異或運算,這樣,就將該key的哈希值全員多參與了運算,會大大降低hash衝突的機率,然後我們再看resize()方法:

其他的一堆判斷我們先不管他,我們看這裏,我們每插入一個數據,它這裏會做判斷,直到next爲null了,就往這個地方存放心的數據,上述代碼我們可以看到他是用e.hash & (newCap - 1)來定位下標索引值的,這裏用了&運算,通過將前面得到的key的哈希值再與當前數組容量的大小減1做與運算,這樣同樣可以獲得0-15的值(假設當前數組容量爲16,不懂&運算的同學可以自行百度一下)。下標定位的問題解決了,我們再看一下,如果出現衝突了他是怎麼處理的。

我們回到putVal();方法:

第一個if分支,大概就是如果這個插入的新值的key和之前存的數值相同,那麼直接用新的值替換老的值,第二個分支如果是樹節點,加入到樹,第三個分支,如果是鏈表遍歷,然後找到最後一個節點,往下加入新值。

接下來,我們先看一下,鏈表是怎麼使用的。如果key值相等就直接替代了,如果key值不相等而hash相等了怎麼辦,這個時候就會在這個數組的節點,延伸出一個鏈表,可以想象爲以這個數組節點爲頭結點,往下用鏈表來存儲,畫一個圖來說明一下:

就是這樣,往下延伸了一個鏈表,我們繼續看,在第三個分支有一個

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);

這裏就是做了一個將鏈表轉換爲紅黑樹的一個方法,具體不展開,我們看一下條件binCount >= TREEIFY_THRESHOLD - 1,binCount就是循環的索引值,上面的代碼可以回去看一下,TREEIFY_THRESHOLD是什麼呢?點進去看得到:

那麼,也就是說當鏈表的長度大於7的時候就轉爲紅黑樹了,我們再看第二個分支

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

這個方法點進去有一個

if (lc <= UNTREEIFY_THRESHOLD)
    tab[index] = loHead.untreeify(map);

UNTREEIFY_THRESHOLD是什麼呢

這裏就是說當紅黑樹的節點小於等於6,就又轉回鏈表了;

這種機制就防止了鏈表太長而導致搜索太慢,而紅黑樹的結構複雜,如果節點少,用紅黑樹可能會更耗資源,在這兩者之間做了平衡。

我們現在思考一個問題,如果這個數組的容量只有16,會出現什麼問題?隨着數據的不斷put,不斷的hash衝突,然後鏈表和紅黑樹的節點越來越多,那麼查找效率肯定是會越來越差的,所以,HashMap還有一個擴容。那麼,什麼情況下它纔會擴容呢?我們回到源碼找一下答案,我們在putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)的方法裏找到這樣一段代碼:

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

先說明一下這兩個參數:

size:
 /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

從源碼註釋可以看出,這個size就是當前map中存在的K-V鍵值對的數量,也就是當前存了多少對鍵值對

threshold:
/**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

先說明一個單詞threshold,百度翻譯得到的結果是:門檻;門口;閾;界;起始點;開端;起點;入門。根據threshold這個單詞的翻譯可以認爲threshold是一個閾值,噹噹前的map容量到達這個閾值就會做一次resize,重新計算容量,也就是擴容。

我們繼續跟進resize()方法,可以找到底下這段代碼:

        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

看一下這幾個變量:

oldThr:原起始點,可以理解爲前一次創建數組的起始點,比如:如果是第一次調用,也就是剛剛初始化,那麼他的閾值就是0,也就是當它的大小爲0的時候就
        觸發一次數組創建,初始化階段創建一個容量爲16的數組。
newCap:新的數組的容量
newThr:新的起始點,可以理解爲將要創建的新的數組的起點,也就是當到達這個閾值的時候,就要新觸發一次數組容量的擴充。
然後我們看最後一句代碼:
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

DEFAULT_LOAD_FACTOR 是擴展因子,源碼中是這樣定義的:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_INITIAL_CAPACITY 前面已經介紹過了,就是默認的容量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

那麼也就是說,當這個閾值到達16*0.75=12 就會觸發一次數組容量的擴充。

我們再回到putVal()方法看

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

這下就清楚了,當map當前的size(當前已經存放了多少鍵值對)大於這個threshold,假設當前容量是16的話,這個threshold就是12,當大於12,就會進行一次擴容操作。

於是乎,又出現一個新的疑問,擴容的容量是多少呢?我們接着在源碼中尋找答案,再一次回到resize()方法:

        if (oldCap > 0) {
            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
        }

當oldCap(原數組的容量)> 0(也就是已經初始化過了),的時候有兩個分支,我們先看第一個

oldCap >= MAXIMUM_CAPACITY
     /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY:最大的容量數。

這個分支就是判斷,是否超過了最大容量,當大於這個數值,就不再進行擴充了。

重點是第二個分支,

(newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY

這裏將原容量左位移1位賦值給了新的容量,左位移1位,也就是擴大了一倍。

現在我們找到答案了,每次擴容爲原來的一倍,但是,如果超過了最大的容量,就不進行擴容了。

 

關於擴容後的數據轉移,我們繼續看源碼,以下是resize()方法中的一段代碼:

                    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;
                        }
                    }

這段代碼不太好形容,我就概述一下它的邏輯,e是一個Node對象,拿e的hash和原數組容量做&運算,如果等於0,就在原數組節點下“掛上”一個鏈表節點,否則,拿當前的索引加上原數組的容量,比如當前索引爲2,原容量爲16,那麼這個Node就會在索引值爲18的數組節點爲頭節點,往下“掛”一個鏈表節點。紅黑樹的數據轉移與鏈表是相似的。

HashMap通過以這種方式處理了隨着容量的擴充,遷移原數組的數據,解決了原結構與新結構的數據分佈不均勻的問題。

 

總結:

1、HashMap在put元素的時候進行初始化,初始化一個容量大小爲16的Node數組。

2、通過對key的hash做一系列的運算,來儘可能的避免hash衝突。

3、由於hash衝突不可避免,當發生hash衝突的時候,有兩種選擇:

      1)如果key相同,直接覆蓋;

      2)key值不同,用鏈表(或紅黑樹)存儲;

4、發生hash衝突(忽略key相同的情況),存儲數值的時候,如果當前鏈表長度小於8則以鏈表形式存儲,如果大於等於8則以紅黑樹存儲;如果紅黑樹的葉子節點小於6,則又會變爲鏈表。

5、擴容因子爲0.75,當新加入的節點的下標超過了,當前數組容量8擴容因子的時候,就會觸發擴容。例如:假設當前數組容量爲16,那麼16*0.75=12,當新加入的值存在第13號索引的時候就會進行擴容操作。擴容主要是爲了避免數組容量太小,隨着hash衝突的不斷髮生,導致鏈表或紅黑樹過於龐大,而導致查找性能低下。

6、擴容之後,擴容前的結構還是相對擁擠,會通過相關算法,將數據進行轉移,一部分留在原地,一部分移到當前索引值加上原數組容量大小的地方。如:當前下標爲2,那麼會有一部分仍留在以數組索引值爲2並以其爲頭節點,往下繼續存儲,另一部分則轉移到數組索引爲18的位置,並以其作爲頭節點,以鏈表方式存儲。這樣就可以使存儲的數據分散開來,方便查詢。

 

 

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