HashMap源碼分析-jdk1.8

首先來看hashmap的幾個構造方法:
loadFactor :負載因子,默認是0.75;簡單來說就是達到當前最大容量 * 負載因子,就該擴容了。

1. Map<String, Integer> map1 = new HashMap<>();
public HashMap() {
		//  static final float DEFAULT_LOAD_FACTOR = 0.75f;
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2. 
Map<String, Integer> map2 = new HashMap<>(10);
Map<String, Integer> map4 = new HashMap<>(10, 0.8f);
//這兩個最終調的都是這個構造方法
// 第一個參數初始容量,第二個參數爲負載因子
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // 負載因子                                     
        this.loadFactor = loadFactor;
        // 閥值 這個地方是直接將容量設置爲閥值了,在具體put時閥值會重新設置爲initialCapacity * loadFactor 
        this.threshold = tableSizeFor(initialCapacity);
    }
// 下面看看tableSzieFor這個方法
// 這個方法的作用了:得到當前 大於等於cap的 最小的  2的n次方數字。
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // static final int MAXIMUM_CAPACITY = 1 << 30;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
// 這個方法先不管
Map<String, Integer> map3 = new HashMap<>(new HashMap<>());

上面的tableSizeFor方法保證了Map的 threshold(閥值)永遠是2的n次方。
比如你傳的是31,則閥值爲32
傳的是5,則閥值爲8

看了構造方法,知道了兩個重要的參數:
loadFactor(負載因子)
爲什麼需要有這個值呢?像ArrayList一樣,佔滿時再擴容不好嗎?
既然是參數,那就說明這個參數沒有那個值在任何情況下都是最好多,不然就不會讓你傳入了;負載因子越大,同樣容量的map裝的內容就越多,但是hash碰撞的機率就越大。

threshold (閥值)
這個值爲什麼一定要是2的n次方呢?
這麼做的原因是便於後面的hash定位(通過key的hashcode對數組取模,就能定位到數組下標),正常來說可以 hashcode % cap ,但是HashMap裏面爲了提高效率,採用的位運算,在具體定位是採用的(n - 1) & hash])並且在擴容時採用的(e.hash & oldCap)來確定新位置能這樣做的前提就是map內容數組長度必須要是2的n次方。

從上面的構造方法並沒有看到多少邏輯,下面我們來看看put()方法的源碼

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

這個方法裏面有個hash(key)方法,這個方法將key的hashcode與hashcode的高16位取異或,目的是爲了減少hash碰撞的機率。,注意key爲null時不會報錯,將hashcode賦值爲0.

 static final int hash(Object key) {
        int h;
        // ^ 表示取異或 將兩個數轉爲二進制,相同爲0,不同爲1
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在看putVal方法前,我們先來看看下面這兩個定義:

//  hashmap內部的數組,構造方法裏面傳入的初始值就是定義該數組的大小,數組裏面存放的Node
transient Node<K,V>[] table;
// 多個Node通過next屬性連接,也就組成了鏈表
// 我們每put一次,就會將內容封裝爲一個Node,然後將Node放入到數組的某個位置,如果數組該位置已經有Node,則會將該Node鏈接到已有Node末尾(會對比,key相同則會覆蓋已有Node),也就成了鏈表。
static class Node<K,V> implements Map.Entry<K,V> {
		// 存放通過hash()方法處理後的hash值,也就是與高16爲取異或,而不會原始的key.hashcode()的值
        final int hash;
        // put時傳入的key
        final K key;
        // put時傳入的value
        V value;
        // hash碰撞時 用於鏈接Node
        Node<K,V> next;
 }

下面來看主要方法putVal();

// onlyIfAbsent 這個參數爲true時表示當key不存在map時才添加,默認爲false,也就是存在時覆蓋
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 第一次put時 table就爲null,所以會進resize()方法,下面會分析resize方法源碼
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // n = tab.length,通過(n - 1) & hash 得到key在數組中的下標,相當於 hash % n
        // 能用(n - 1) & hash的前提就是n必須爲2的n次方
        // 如果數組該位置沒有內容,則直接newNode,設置到數組該位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 表示數組該位置已經有Node, p爲該位置上鍊表的頭節點
            // 判斷兩個Node是否相等,則需要保證hash相等並且 用== 或者equals比較也相等
            // 這也是當對象爲
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 如果相等 將頭節點賦值給e,後續直接覆蓋該節點的value
                e = p;
            // 如果是紅黑樹節點,則走tree的put邏輯
            else if (p instanceof TreeNode)
                // 這個方法有點複雜,本次不分析
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 到這 就需要遍歷鏈表,判斷key是否存在
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 表示鏈表遍歷完了也沒有找到
                        // newNode,放到鏈表最後面
                        p.next = newNode(hash, key, value, null);
                        // static final int TREEIFY_THRESHOLD = 8;
                        // 當鏈表長度達到8,則要判斷是否轉化爲紅黑樹,後面會看treeifyBin方法源碼
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                     // 找到了key相同的Node
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 遍歷完一次沒找到,將p.next 賦值給p,繼續遍歷
                    p = e;
                }
            }
            // 已經存在key相同的Node
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    // 將Node值設置爲當前傳進來的值
                    e.value = value;
                // 模板方法,空實現,用於擴展
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // modCount記錄map變動,用於在forEach方法中會判斷遍歷時map是否有變動,如果有變動則會拋出ConcurrentModificationException
        ++modCount;
        // 如果當前size已經>閥值,則需要擴容
        // 這裏爲什麼不寫成當size==threshold就擴容呢? 原因是在多線程環境下可能出現size瞬間大於threshold(比如從11直接到13),這樣就一直沒法滿足擴容條件了。
        if (++size > threshold)
            resize();
         // 模板方法,空實現,用於擴展
        afterNodeInsertion(evict);
        return null;
    }

上面流程中有兩個方法,需要進一步分析:

resize()和treeifyBin(tab, hash)方法

下面先看treeifyBin(tab, hash)方法,當數組中某個位置的鏈表長度達到了8就會調用此方法,調用此方法是不是肯定會將鏈表轉爲紅黑樹呢?
當然不是,看代碼:

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // static final int MIN_TREEIFY_CAPACITY = 64;
        // 如果數組大小 < 64,則調用的是擴容的方法
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

看了上面方法,可以看出只有當 數組某個位置鏈表長度達到8 並且 當前table的長度大於等於64,纔會將鏈表轉化爲紅黑樹

最後來看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;
            }
            // newCap = oldCap << 1 將新的數組大小設置爲原來的兩倍
            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
        	// 第一次put時,有傳初始值,new HashMap(8),則會出現oldCap =0,oldThr=8
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
           // 第一次put,並且調用無參構造方法: new HashMap();
           // static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
            newCap = DEFAULT_INITIAL_CAPACITY;// 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 12
        }
        if (newThr == 0) {
            // 會重新計算閥值,然後賦值給threshold
            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;
                    // 當數組位置只有一個Node時,直接算出在新數組的下標,然後賦值
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    	// 紅黑數的處理
                    	// 會將紅黑樹拆分爲兩個鏈表,然後計算在新數組中的位置,隨後會根據鏈表長度來判斷
                    	// 如果鏈表長度<=6 (UNTREEIFY_THRESHOLD = 6),就會將紅黑樹轉爲鏈表,否則轉爲紅黑樹。
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 定義了兩組鏈表 loHead和loTail表示e.hash & oldCap == 0 的頭和尾
                        // hiHead和hiTail表示e.hash & oldCap != 0的鏈表頭和尾
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 爲0表示在舊數組中的位置和在新數組中的位置一致
                            // 只所以能夠這麼算,必須保證oldCap是2的n次方,並且newCap是oldCap的兩倍
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 不等於0的表示:Node位置=在舊數組中的位置+oldCap,比如舊數組長度爲8,當前節點在舊數組位置爲3,則擴容後在新數組的位置爲8+3=11.
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            // 等於0的 直接賦值給新數組(下標和在舊數組中的位置一樣)
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // 不爲0的則需要 舊數組位置+oldCap
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

擴容過程總結:
1.不需要對舊數組的Node全部重新計算Hash,而是通過(e.hash & oldCap) 是否等於0來確定Node在擴容後數組的下標。爲0時在新數組中的位置和老數組一致;不爲0時在新數組的位置爲 在舊數組的位置+舊數組長度。這樣計算的前提時oldCap是2的n次方,newCap爲oldCap二倍,大大提高了hashMap的擴容效率。(感嘆設計的精妙)
2,採用兩組鏈表分別保存(e.hash & oldCap)爲0和不爲0的,鏈表的元素順序和在舊數組中鏈表元素順序一致,不存在jdk1.7中擴容時鏈表順序反轉,多線程情況下可能導致死循環的問題。

那在什麼情況下,會將紅黑樹又轉回鏈表呢?
// 下面我們看看紅黑樹的擴容邏輯 ,也就是上面的((TreeNode<K,V>)e).split(this, newTab, j, oldCap);方法

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
            	// static final int UNTREEIFY_THRESHOLD = 6;
            	// 小於等於6 轉回鏈表
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                    	// 否則轉爲紅黑樹 
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

紅黑樹的擴容會拆分爲兩個鏈表,當某個鏈表長度<=6,就會轉爲鏈表,否則轉爲紅黑樹。

到這,主要分析了HashMap的put流程和擴容流程。

最後我們來做個驗證,驗證一下HashMap在hash定位 和擴容時的位運算和預期結果對不對。
首先來驗證hash定位,用的是(n-1) & hash,n表示數組長度,這算式的值應該和 hash % n相同。看下面:
在這裏插入圖片描述
說明結果確實和用hash 對數組長度取餘一樣的。

下面來驗證擴容時,擴容採用的 e.hash & oldCap,oldCap爲擴容前數組長度,如果計算結果爲0,則表示在新數組中的位置和在舊數組中一樣;如果不爲0,則表示在新數組中的位置= oldCap + 在舊數組中的位置,驗證如下:
在這裏插入圖片描述
驗證也通過,HashMap在擴容時並沒有通過hash重新計算在新數組中的下標,而是以這種巧妙的設計得到正確結果,提高了擴容效率。

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