­

Debug HashMap

最近跟兩個正在找工作的同學聊天,說起集合,都是面試的重災區,必問的選項,而且在實際的面試中並不會單獨提問某一個問題,而是圍繞核心知識連環炮提問。所以背面試題治標不治本,還是得讀一讀源碼。誰讓這是個面試造火箭,工作擰螺絲的市場氛圍,就連CSDN的首頁第二張輪播圖都在蹭這個熱點:

image-20200715110439001

本文主要包括兩部分:

  • HashMap面試必問(總結了一些常見面試題)

  • JDK1.7 & JDK1.8 關於HashMap原理分析

    這部分主要是通過斷點debug來分析HashMap中常見操作的過程,但由於步驟繁多,只記錄了關鍵步驟,建議讀者也在自己電腦上debug一遍,了解詳細流程。(計算機是一門實踐性很強的學科,看的再多也不如自己親自操作一遍,當然理論也同樣重要)

長文警告!!!

1,HashMap面試必問

這是筆者在一篇博客中找出來的,很有代表性,實際的面試提問中不會按部就班的問,而是千變萬化,所以除了把面試題背住之外,一定要花點時間看看源碼具體實現,雖然不會360度無死角,但對源碼總體有個大概的把握,回答起來就知道哪些知道哪些不知道,一來方便查漏補缺,二來也能更加靈活的回答問題。

示例性提問(真實場景下):

  • 你看過JDK的源碼嗎?

    看過。

  • HashMap是如何通過put添加元素的?

    根據key計算hash值,再將hash值轉換為數組下標。

  • 底層數組默認的長度為多少?

    默認為16。

  • 什麼時候會觸發擴容機制?

    元素個數超過閾值就會觸發擴容機制,並且是在新增元素髮生hash衝突的情況下。

  • 擴容時,直接將數據從原數組平移到新數組可以嗎?

    不行,需要重新計算hash值(更正,是重新計算index值,而不是重新計算hash值,hash值只與key相關,index與table.length相關)

  • 為什麼需要重新計算hash值?

    因為數組擴容了,從hash值轉換為數組下標這個過程就發生了變化,同時,獲取value這個過程也會發生變化。所以必須重新計算,不然之前保存的元素就無法訪問。

一般性問題(建議背住,而後融會貫通):

  • 什麼是HashMap?

    HashMap是基於Map接口的實現,主要用於存儲鍵值對(1.7通過Entry對象封裝鍵值對,1.8通過Node封裝鍵值對)

  • HashMap採用了什麼數據結構?

    1.7:數組+鏈表

    1.8:數組+鏈表+紅黑樹

  • HashMap是如何解決hash衝突的問題的?

    鏈表。

  • hash衝突和index衝突的關係?

    hash衝突就會導致index衝突,indexFor方法的兩個參數一個是hash值,另外一個是table.length。

  • HashMap的put方法是如何實現的?

    先通過key計算hash值,再通過indexFor方法轉換為數組下標。

  • HashMap的擴容機制是什麼樣的?

    HashMap默認初始容量為16,加載因子為0.75,實際存儲大小為12。hashMap容量達到12並且當前加入的元素產生hash衝突時時,進行初始容量的2倍擴容

    • 為什麼初始容量為16?

      HashMap重寫的hash採用的是位運算,目的是使key到index的映射分佈更加均勻

      	static final int hash(Object key) {
              int h;
              return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
          }
          
          也解釋了為什麼hash允許空值,實際上當key為null時,自動轉換為0
      
  • 為什麼鏈表使用頭插法?

    HashMap的發明者認為,後插入的Entry被查找的可能性更大

  • hashMap中的鏈表是單鏈表還是雙鏈表?

    單鏈表

     		final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
  • 擴容閾值threshold被賦值了幾次?

    • 調用構造函數被賦值,初始化容量大小(默認為16)
    • 數組為空,初始化數組時,被賦值為初始化容量*加載因子(默認為12)
  • hash衝突插入鏈表的方式?

    1.7:採用頭插法:作者認為,後插入的會被優先訪問

    1.8:採用尾插法:避免鏈表死循環

  • hashMap允許key為null值嗎?

    允許一個key為null,會轉換為數組下標0。當出現第二個key為null,其value會自動覆蓋第一個null的值。

  • hashMap中鏈表過長會導致什麼問題?

    查詢效率降低。時間複雜度為O(n)【需要遍歷鏈表】

  • jdk7中的HashMap存在哪些問題?

    • 鏈表過長導致查詢效率降低

    • 擴容導致的死循環

    • 線程不安全(個人認為這不是問題,而是在設計上就沒有考慮這個,線程安全就會導致效率降低,本質上是效率和安全之間的取捨)

  • jdk7和jdk8處理hash衝突的區別?為什麼?

    jdk7計算hash值的運算是非常複雜的,因為如果產生了hash衝突是用鏈表來進行存儲的,效率比較慢,所以在設計上要儘可能避免衝突。

    jdk8計算hash值的方法相對簡單,因為採用了紅黑樹的結構,即使發生了hash衝突,也可以通過轉換為紅黑樹來提高效率。

  • 為什麼加載因子是0.75而不是其他值?

    因為加載因子參與indexFor數組下標的計算,return h & (length-1);

    其數值會影響index是否發生衝突,同時也會影響空間利用率,默認情況下table長度為16,但只能存12個值。

    所以這個加載因子是在index衝突和空間利用率之間尋求的一個平衡點。

  • HashMap是否可以存放自定義對象?

    可以,因為HashMap使用了泛型。

  • 為什麼JDK8引入紅黑樹?

    由於hash衝突導致鏈表查詢非常慢,時間複雜度為O(n),引入紅黑樹後鏈表長度為8時會自動轉換為紅黑樹,以提高查詢效率O(logn)。

  • Java集合中ArrayList,LinkedList,HashMap的時間複雜度分別為多少?

    ArrayList基於數組實現,基於下標查詢的話時間複雜度為O(1),如果基於內容查找需要遍歷的話,時間複雜度為O(n)。

    LinkedList基於鏈表實現,查詢效率為O(n)

    HashMap在不考慮Hash衝突沒有形成鏈表的情況下時間複雜度為O(1),形成鏈表後時間複雜度為O(n)

2,Debug源碼的心得體會

【關注核心步驟,選擇性忽略】

JDK是一個相當龐大的系統,把所有的類和原理全部弄清楚是相當有難度的,所以在debug源碼的時候,如果遇見了不相關的類,忽略就是了。

然而單看HashMap源碼(2300行)也是一個較為龐大的代碼量,所以對其中不重要或者不常用的方法,最好先選擇性忽略。比如計算hash值的各種位運算,研究起來還是得廢一些功夫的,這個可以在把握了HashMap的大致框架後再做精細化的研究。

總的來說,先重點關注核心步驟,選擇性忽略更加具體的實現,逐個擊破,從而提高閱讀效率

ps:建議把1.7和1.8的jdk都裝上,切換着分析。

3,JDK 1.7

3.1 用debug分析一個元素是如何加入到HashMap中的【jdk1.7】

創建一個Main.java類

 		HashMap<String,String> hashMap = new HashMap<>(16);
        
        hashMap.put("x","x");
        hashMap.put("y","y");

在創建HashMap對象上打上斷點:

image-20200715162215255

debug運行,強制進入方法內部(Alt+Shift+F7):

調用構造函數:

image-20200715165233458

this方法,初始值判空異常(初始值不能小於0大於最大值),加載因子判空異常,

threshold被初始化容量賦值(threshold為擴容閾值)

image-20200715165318136

在插入第一個元素上打上斷點:

image-20200715165820913

debug運行,強制進入方法內部(Alt+Shift+F7):

	public V put(K key, V value) {
		//判斷數組是否為空,如果為空進行初始化,inflateTable初始化方法見下文①
		//threshold:擴容的閾值(當前元素個數超過這個數值就會進行擴容)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        
        //判斷key是否為空
        if (key == null)
        	//hashMap處理空值的方法②
            return putForNullKey(value);
            
        //計算key的hash值(主要是各種位運算)
        int hash = hash(key);
        
        //i就是將key的hash值再進行一次轉換得出的數組下標
        int i = indexFor(hash, table.length);
        //同樣是個處理hash衝突的頭插算法
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        
        //添加元素③
        addEntry(hash, key, value, i);
        return null;
    }

①inflateTable初始化容量方法:

private void inflateTable(int toSize) {
        //向上舍入為2的冪
        int capacity = roundUpToPowerOf2(toSize);

	    //重點:threshold在初始化構造函數時默認為16,在初始化數組時,乘以加載因子被二次賦值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化數組容量
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

②hashMap處理空值的方法

private V putForNullKey(V value) {

		//處理key為null值的hash衝突,採用頭插法(null會自動轉為0)
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

③addEntry添加元素

void addEntry(int hash, K key, V value, int bucketIndex) {
		//hash擴容(size代表元素個數,如果元素大於threshold【默認是12】,則會進行擴容)
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//④
        createEntry(hash, key, value, bucketIndex);
    }

void createEntry(int hash, K key, V value, int bucketIndex) {

		//bucketIndex就是put方法中計算出的數組下標i
		//難點:如果未發生hash衝突,table[bucketIndex]則為空,e也為空,table[bucketIndex]等於最新插入的元素
		//如果發生了hash衝突,也就是table[bucketIndex]並不為空,table[bucketIndex]就頭插到鏈表中
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

3.2 用debug分析HashMap是如何get到一個元素的【jdk1.7】

還是先編寫測試用例:

ps:測試的代碼都不複雜,關鍵是要關注底層是如何實現的

  		HashMap<String,String> hashMap = new HashMap<String, String>(3);

        hashMap.put("x","x");
        hashMap.put("y","y");
        hashMap.put("z","z");
        hashMap.get("z");

打上斷點:

image-20200716104509610

debug運行,強制進入方法內部(Alt+Shift+F7):

public V get(Object key) {
        if (key == null)     //判空
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);  

		//判空,否則返回value
        return null == entry ? null : entry.getValue();
    }
final Entry<K,V> getEntry(Object key) {
		//判斷數組是否為空
        if (size == 0) {
            return null;
        }

		//判斷key是否為空,為空則返回0,否則計算hash值
        int hash = (key == null) ? 0 : hash(key);
        
        //遍歷鏈表,獲取Entry對象
        for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
            Object k;
            
            //核心:hash相等並且key相等才能返回entry,否則繼續遍歷
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

3.3 用debug分析HashMap是如何擴容的?【jdk1.7】

編寫測試用例:給的初始值為3,根據2的冪計算,HashMap初始化容量為4,擴容閾值為3,也就是在執行 hashMap.put(“m”,”n”);時會發生擴容:

		HashMap<String,String> hashMap = new HashMap<String, String>(3);

        hashMap.put("x","x");
        hashMap.put("y","y");
        hashMap.put("z","z");
        hashMap.put("m","n");

打上斷點:

image-20200715204334725

debug運行,強制進入方法內部(Alt+Shift+F7):

判斷數組是否為空。false

image-20200715204404918

。。。(此處省去一些步驟)

運行到addEntry方法對size和threshold進行判斷,此時size為3,滿足條件。(ps:除了當前大小大於等於閾值之外,當前元素計算出的數組下標也必須與之前的元素產生hash衝突才能擴容)

【坑點】:size是元素總個數,而不是數組佔用個數,比如只佔用了一個數組位置,但是鏈表長12,還是會擴容,其目的是使得hash分佈的更均勻

resize方法對數組table進行兩倍擴容,當前table.length = 4.

image-20200715204509129

resize方法:

image-20200715204719762

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));    //將數據移至新數組⑤
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

⑤將數據移至新數組

/**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍歷鏈表
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);   ///重新計算數組下標
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

3.4 HashMap 1.7 中多線程下擴容的死循環問題

問題描述:jdk1.7在多線程並發的情況下會由於鏈表的頭插法導致擴容的死循環問題,在1.8中已經被解決。

問題代碼:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        
        //table是全局變量,多線程的情況下,由於沒有任何鎖的機制,多個線程可以同時獲取到table
        for (Entry<K,V> e : table) {    
        
        //遍歷鏈表
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //重新計算hash值
                int i = indexFor(e.hash, newCapacity);
                //頭插法插入鏈表
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

圖片描述:假設有A,B,C,D四個元素組成的鏈表,在擴容的時候,遍歷鏈表A最先被移過去,其次是B,C,D,假設在進行擴容前,同時有兩個線程獲取到了全局變量table,T1線程擴容進行到了如圖所示的步驟,正準備移動D過去。T2線程此時獲取到的table的仍然擴容前的指向。所以T2讀取到的table可能是A指向B,B同時指向A,這種情況下,遍歷鏈表就會導致死循環。

			   e.next = newTable[i];
                newTable[i] = e;
                e = next;
                
                一個元素的移動過程(index衝突),newTable[i]是已經移到新table中的數組下標對應的元素,如下圖所示,C這個時候就是newTable[i],e
               就是D,那麼過程就是D指向了C,然後把e也就是D元素賦給newTable[i],此時這個鏈表的頭結點就是D。最後一行代碼相當與e = e.next。繼續遍歷鏈表。

image-20200717145934315

4,JDK1.8

1.8相對於1.7有很多改進,比如採用了新的數據結構紅黑樹,鏈表改為尾插法等等。相對來說,1.8的代碼量較1.7更多,故下文會部分省略代碼,只展示程序運行過的步驟。

4.1 用debug分析第一個元素是如何加入到HashMap中的【jdk1.8】

切換到jdk1.8,繼續debug

image-20200717152242240

image-20200717152332868

計算hash函數:hash(key),1.8中同樣允許null值,會自動轉換為0

image-20200717152349422

jdk1.7中計算hash的方法
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

jdk1.8中計算hash的方法
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
jdk1.7中計算hash值的方法相對比較複雜,主要是因為要儘可能的避免hash衝突,因為鏈表的遍歷是很慢的。但jdk1.8中因為引入了紅黑樹,即使hash衝突很高,也可以通過轉換紅黑樹來提高查詢效率。(所以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;
        
        //初始化擴容 ,resize方法見下文
        if ((tab = table) == null || (n = tab.length) == 0)
       		//n為擴容後的容量,本次情況下為4,上文中HashMap的初始化容量設為3,根據hashMap規則,容量只能為2^n
            n = (tab = resize()).length;
        //&優先級高於=,看了半天沒明白啥意思,1.7中將hash轉換為index的過程用indexFor方法封裝起來了,其實是一樣的:h&(length-1)
        //如果當前位置是空的,直接賦值給數組
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
         //這裡包括轉換為鏈表或紅黑樹,下文再分析
        else {
            **************
        }
        //修改次數+1
        ++modCount;
        
        //若當前size+1後的值大於擴容閾值,執行擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
//hashMap擴容方法
final Node<K,V>[] resize() {
		//獲取到當前table,table是全局變量
        Node<K,V>[] oldTab = table;
        //計算當前table的長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //獲取當前擴容閾值(threshold=capacity*loadFactor)
        int oldThr = threshold;
        //初始化新的容量和擴容閾值
        int newCap, newThr = 0;
        if (oldCap > 0) {
        //若當前容量大於最大容量(10億多)
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //左移運算符優先級高於賦值運算符,左移1位相當於乘以2,newCap相當於舊容量2倍擴容
            //另外一個判斷條件:當前容量大於默認容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //新的擴容閾值翻倍
                newThr = oldThr << 1; // double threshold
        }
        //若當前擴容閾值大於0
        else if (oldThr > 0) // initial capacity was placed in threshold
        //將當前擴容閾值賦值給新容量
            newCap = oldThr;
            
        //若當前容量為0且擴容閾值為0,這種情況是在沒有給hashmap任何初始值的時候發生的
        else {               // zero initial threshold signifies using defaults
            //默認容量為16
            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;
        
        //在為空初始化容量時,並不會進入分支,下文再補充注釋
        if (oldTab != null) {
            *******
        }
        //返回新的鍵值對數組
        return newTab;
    }

ps:1.8中使用Node代替Entry,換了個名,然後hash加上了final修飾

image-20200717153631434

image-20200717153646368

4.2 用debug分析HashMap擴容情況【jdk1.8】

測試用例如下:HashMap的初始容量給到3,實際容量為4,擴容閾值為3,在添加第四個元素的時候進行擴容

image-20200727115443539

進入方法內部:

image-20200727115750636

重點關注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;
        //table為空時初始化的擴容操作
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            
        //若當前數組下標並未有元素,直接賦值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            
            //形成鏈表
        else {
        
            Node<K,V> e; K k;
            //若key衝突,直接替換value(key相同,hash值一定相同)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷是否形成紅黑樹
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                
            //判斷是否形成鏈表
            else {
            	//遍歷當前table[i]所在的鏈表
                for (int binCount = 0; ; ++binCount) {
                *******
                }
            }
        }
        ++modCount;
        //當前size為3,加1後大於擴容閾值,進行擴容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize()擴容:

final Node<K,V>[] resize() {
		//獲取到當前table,table是全局變量
        Node<K,V>[] oldTab = table;
        //計算當前table的長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //獲取當前擴容閾值(threshold=capacity*loadFactor)
        int oldThr = threshold;
        //初始化新的容量和擴容閾值
        int newCap, newThr = 0;
        if (oldCap > 0) {
        //若當前容量大於最大容量(10億多)
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //左移運算符優先級高於賦值運算符,左移1位相當於乘以2,newCap相當於舊容量2倍擴容
            //另外一個判斷條件:當前容量大於默認容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //新的擴容閾值翻倍
                newThr = oldThr << 1; // double threshold
        }
        //若當前擴容閾值大於0
        else if (oldThr > 0) // initial capacity was placed in threshold
        //將當前擴容閾值賦值給新容量
            newCap = oldThr;
            
        //若當前容量為0且擴容閾值為0,這種情況是在沒有給hashmap任何初始值的時候發生的
        else {               // zero initial threshold signifies using defaults
            //默認容量為16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //默認的擴容閾值為默認的負載因子乘以默認初始化容量
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //若新的擴容閾值為0
        if (newThr == 0) {
        	//計算新的擴容閾值:在新容量小於最大容量且計算後的擴容閾值小於最大容量的情況下,新的擴容閾值為新容量乘以負載因子,否則為最大容量
            float ft = (float)newCap * loadFactor;
            
            //此時新的擴容閾值為6,容量為8
            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;
        
        //上文補充,此時舊數組並不為空 ***************************************************************************//
         if (oldTab != null) {
         	//遍歷舊數組,遍歷計算下標放入新數組中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //null會直接轉化為0,所以不需要計算
                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
                    
                    //為鏈表節點,需要進行重hash分佈(就是數組下標的重新計算,一天天的,就不整個人話)
                        Node<K,V> loHead = null, loTail = null;   //用於數組下標為0的節點
                        Node<K,V> hiHead = null, hiTail = null;   //用於數組下標發生變化的節點
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //將當前元素的hash值與老表的容量進行與運算,相當於計算數組下標,若等於0,則擴容後的下標仍然是0
                            if ((e.hash & oldCap) == 0) {
                            	//若loTail為空,表示該節點為鏈表上的第一個節點(loTail表示鏈表尾),將節點賦給loHead
                                if (loTail == null)
                                    loHead = e;
                               //若loTail不為空,表示當前節點並非是鏈表的第一個節點,可將e賦給鏈表尾loTail的下一個指向,此時表尾lotail後連接的是e
                                else
                                    loTail.next = e;
                                    
                                //將e賦給鏈表尾,1.8中使用了尾插法,而1.7中使用的是頭插法
                                loTail = e;
                            }
                            //處理數組下標非0的節點
                            else {
                            //同理:使用尾插法連接節點
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);    //這個循環就是遍歷鏈表,直到下一個為null
                        
                        //如果loTail不為空,說明老數組中的數組下標在新數組中也有使用
                        if (loTail != null) {
                        	//將鏈表尾的下一個指向置為空
                            loTail.next = null;
                            //將鏈表頭賦值給新數組的元素
                            newTab[j] = loHead;
                        }
                        
                        //如果hiTail不為空,說明這是非0的數組下標,
                        if (hiTail != null) {
                        	//將鏈表尾的下一個指向置為空
                            hiTail.next = null;
                            //新數組下標為原來的數組下標+舊容量(666)
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回新的鍵值對數組
        return newTab;
    }

4.3 用debug分析鏈表的形成過程【jdk1.8】

編寫測試用例,(???如何模擬更多的hash衝突???)

image-20200727143057762

image-20200727143216152

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //table為空時初始化的擴容操作
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            
        //若當前數組下標並未有元素,直接賦值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            
        //形成鏈表**************************************************
        else {
      
            Node<K,V> e; K k;
            //若key衝突,直接替換value(key相同,hash值一定相同)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷是否形成紅黑樹
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                
            //排除了key覆蓋和紅黑樹,剩下的就是鏈表了
            else {
            	//遍歷當前table[i]所在的鏈表
                for (int binCount = 0; ; ++binCount) {
                	//若鏈表當前節點的下一個節點為空,說明已到鏈表尾,break退出循環
                    if ((e = p.next) == null) {
                    	//退出循環前,把新元素加到鏈表尾部
                        p.next = newNode(hash, key, value, null);
                        //若鏈表節點數量大於等於8,轉換為紅黑樹(binCount從0開始計算,到7的時候已經是第8節點了)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
         
        }
        ++modCount;
        //當前size為3,加1後大於擴容閾值,進行擴容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

4.4 用debug分析get元素的過程【jdk1.8】

image-20200727151534041

image-20200727151615637

getNode()

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) {
            
            //判斷第一個節點的hash值和key是否相等,若相等,直接返回,否則進入鏈表遍歷
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //遍歷鏈表
            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;
    }

4.5 用debug分析刪除元素的過程【jdk1.8】

image-20200727152416237

image-20200727152441206

removeNode()

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //一個if看得都費勁,p節點是根據hash和key計算出的待刪除的節點
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //若p的hash和key都吻合,直接賦值節點node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
                
            //說明p所在節點為一個鏈表
            else if ((e = p.next) != null) {
            	//判斷鏈表是否轉換成了紅黑樹
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                //若未轉換為紅黑樹,則遍歷鏈表,直到key和hash都吻合,賦值給node
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            //刪除node
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
               //判斷node是否為紅黑樹節點
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //判斷node節點是否為鏈表的第一個節點,若是,將當前鏈表的下一個節點指向賦給數組
                else if (node == p)
                    tab[index] = node.next;
                //最後一種情況就是node節點在鏈表中間,將頭節點的下一個節點指向node的下一個節點。
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                //返回node
                return node;
            }
        }
        return null;
    }

get和remove的思路

兩者大體思路相同,先根據傳入的key計算hash,再依次通過:第一個元素是否命中,鏈表是否為紅黑樹,遍歷鏈表的思路尋找對應的節點元素刪除或返回。

4.6 關於紅黑樹。核心就是自平衡!

紅黑樹基於二叉查找樹實現,在此基礎上做了優化。

二叉查找樹又稱二叉搜索樹,二叉排序樹

關鍵規則如下:左子樹的值=<根節點=<右子樹的值,左右子樹遵守同樣的規則

二叉查找樹的平衡問題:

image-20200727155549666

紅黑樹的核心功能就是自平衡。

紅黑樹的規則:

  • 節點為紅色或黑色

  • 根節點是黑色

  • 葉子節點(NIL)是黑色

  • 如果一個節點是紅色的,則它的子節點必須是黑色的。

  • 任一節點到其子樹的葉子節點的路徑都包含相同的黑色節點

preview

新插入的節點是這樣的:

image-20200727160137762

若向當前樹中插入14,則為:並不會引起紅黑樹的變化

preview

但若插入節點為21:違反了紅黑樹的紅色節點的子節點都為黑色

img

與規則發生衝突時,紅黑樹需要進行調整,調整有兩種方式:變色和自旋(自旋又分為左旋和右旋)

變色:比如新添加一個紅色節點到一個紅色節點下就會產生變色的情況。

左旋:當前節點變為左節點,當前節點的右節點變為父節點(把右節點的子樹的左節點往左子樹挪)

img

右旋:當前節點變為右節點,當前節點的左節點變為父節點(把左節點的子樹的右節點往右子樹挪)img

4.7 hashMap樹化原理

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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                
            //若當前已經是紅黑樹,直接向樹中添加元素
            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);
                        //若鏈表長度大於8,轉換為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
    }

樹化方法 treeifyBin(tab, hash);

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        
        //若table為空或者tab的長度小於樹化最小長度,優先擴容
        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;   //定義紅黑樹的頭結點和尾結點
            //遍歷鏈表,最終結果:hd為表頭,tl為表尾
            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);
            //將hd賦給數組
            if ((tab[index] = hd) != null)
            	//樹化方法
                hd.treeify(tab);
        }
    }

treeify

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            //遍歷鏈表,this在第一次循環代表hd
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //初始化根節點
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //遍歷根節點
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;   //為p的左子樹
                        else if (ph < h)
                            dir = 1;   //為p的右子樹
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        //判斷p的子樹是否為空(賦值和判斷同時進行,666),若不為空,則在其子樹下繼續循環。最後到達葉子節點,插入節點
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x); //自平衡
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

本文篇幅已經過長,關於紅黑樹,之後會專門寫一篇文章研究1.8中的實現。