實戰系列-HashMap深入剖析

導語
  手撕面試官,面試某公司開發的時候被問到了HashMap底層,問到我懷疑人生,不知道是面試官錯了還是我錯了。我相信是我錯了利用下班時間來分析手撕一下HashMap。

  通過手撕源碼加上實驗來給自己打臉,這些東西你跟面試官真的懂麼?想要做出創新的東西真的容易麼?就靠着自己對於增刪改查就可以馳騁江湖了麼?自己離高手的路還有一段距離。

實驗

  不用多說直接上源碼這裏,首先解釋一下傳入的幾個參數。HashMap 傳入的就是KV兩個值,沒有什麼好解釋的,進入方法之後調用的是putVal的方法這裏可以看到。調用了一個hash()的方法。

  /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

到這裏第一個問題來了:這個HashCode是怎麼產生的?
   到這裏有面試官會問這個時候我們的hashCode是怎麼產生的。就是通過這個方法,這裏需要通過 (h = key.hashCode()) ^ (h >>> 16) 一個操作,這裏解釋一下這個操作,希望讀者可以擦亮眼睛。看清楚了
hashCode()方法是native方法,而^操作 表示異或,而 >>> 這個操作,我們都知道>> 這個叫做左移

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

什麼是左移,下面這個操作就是左移

原來數據是   1 1 1 1  十進制表示 15
左移操作  0 1 1 1 十進制表示 7
左移兩位 0 0 1 1 十進制表示 3

測試一下
  通過上面的分析來測試一下上面這個方法返回的值是什麼

public class Test {

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    public static void main(String[] args) {
        
        int hash = hash(10);
        System.out.println(hash);  // 10 
    }
}

分析一下

10 十進制  二進制表示 1010

首先第一步操作獲取了一個HashCode 假設這個值就是 10

那麼這裏的第一個操作就是 h = key.hashCode() 獲取到 h = 10;
第二個操作 就是 h >>> 16 ,結果爲 0 二進制也是 0   最終將兩個值做了異或 異或的意思就是 只要有1 就是1 

所以 最後結果還是 10

  到這裏就應該就差不多了,再要往底層問就是要問一下hashCode()方法體內部是什麼操作?有興趣的可以瞭解一下。

  繼續往下就進入了核心方法中,這裏就有好日子過了。

putVal()

  
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key 通過hash計算的hash 值
     * @param key the key 具體存入的鍵值
     * @param value the value to put 放入的value值
     * @param onlyIfAbsent if true, don't change existing value 如果爲true ,則不去改變現有值 傳入的值是 false
     * @param evict if false, the table is in creation mode. 如果爲false,則表處於創建模式,傳入的值爲 true
     * @return previous value, or null if none
     */
     
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //建立新節點數組,建立節點,初始值
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //首先判斷 tab 以及實際存儲的table是否爲空,第二步就是看 tab的長度是否爲0。
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 重新賦值 重置之後會獲取到一個新的數組
            n = (tab = resize()).length;
        // 如果  tab[i = (n - 1) & hash] 對應的節點爲空,這個怎麼理解?
        // 首先來分析一下這個時候 n 的值應該是新創建的數組的長度,拿默認值來說這個長度應該是 16 
        // 這裏 n-1 也就是 15 例如放入這個位置的值爲 10 也就是 1010 和 1111 做一個與操作,根據分析可以知道 它應該就是 1010 那麼就知道這裏判斷的是這個數組第10個位置是否有數據,如果沒有數據則進行一個 newNode(hash, key, value, null)操作也就是創建一個新的節點。源碼方法很簡單就是創建一個Node 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        	//否則就是由Hash衝突了,這裏就需要解決這個衝突,首先想到的就是將該節點上的衝突轉移到鏈表中。
            Node<K,V> e; K k;
            // 這裏進入hash的判斷,假設一個很極端的情況來進行分析,就是存儲的所有元素都是10 
            // && 這操作是一個與操作,也就是這兩邊的表達式操作都要爲true纔可以。那麼首先來看
            //p.hash == hash 如果上面這個場景成立,那麼10 和10 的hash應該是一樣的,條件成立
            // ((k = p.key) == key || (key != null && key.equals(k)))) 這個表達是很長但是可以根據括號做一個拆分。
            //(k = p.key) == key 還是之前的極端情況這裏的key一樣 這個條件爲真 
            // || 表示或也就是說這個符號左右兩邊只要有一個爲true就可以了。
            // (key != null && key.equals(k))) 繼續就是判斷這個key的值是否一樣,這裏有一點需要注意就是,如果放入的就是 大量相同數據那麼這個地方條件都成立了,進入的操作應該是e = p; 也就是說如果所有條件都成立了那麼最終的結果應該是這個 p 節點就是 e 節點 直接做了指針交換。也就間接的證明一件事情,如果key相同,那麼存儲到HashMap中的內容無論有多少,只要key相同就看作一個。這個就是爲什麼在HashMap中的Key不能重複的原因。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 做完上面的判斷之後,就開始做TreeNode的判斷是否是TreeNode,這裏我們知道,這個節點 p 判斷的時候 並不是一個TreeNode,而是一個Node那麼這個條件是不是一直不成立呢?
            // 首先  Node 實際上繼承了Entry 而 TreeNode繼承的是LinkedHashMap.Entry<K,V> 也就是HashMap的鏈表實現。
            // 第一點 p 節點 在什麼時候會變成 TreeNode。這個就和resize()中的split()方法有關,這裏先不做分析
            // 第二點 變化節點類型之後put的方式也發生了變化。就是下面,這個操作。  
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 如果整個的條件都不滿足,也就是說既不是樹節點,也不是同一個key,這個時候就需要進入下下面這個邏輯   
            else {
            	// 這裏是一個死循環
                for (int binCount = 0; ; ++binCount) {
                    // 判斷如下,首先獲取到p節點的下一個節點判斷是否爲空
                    if ((e = p.next) == null) {
                        // 如果爲空 則創建新的鏈表節點。使用鏈表的尾部插入法
                        p.next = newNode(hash, key, value, null);
                        // 判斷一下 計數是否大於等於TREEIFY_THRESHOLD - 1 這裏其實就是轉換紅黑樹的臨界區,當鏈表長度大於8的時候就進行了一個treeifyBin(tab, hash);操作,看看具體傳入的參數,一個是tab ,另個一是hash,這個hash就是傳入的通過 hashCode()計算的哪個值。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 分析完這個地方的邏輯之後,可以看到,完成轉換之後的操作還是與整個的數組擴容沒有關係,就是解決數組的hash衝突的問題。最終進行返回
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果e節點的hash值 就是滿足下面這個條件也是解釋了上面分析中的一個key唯一的問題。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 這個時候將e節點賦值給p節點。其實就是拿到這個節點做了一個轉換,如果轉換沒成功則就還是原來的東西還給原來的位置。    
                    p = e;
                }
            }
            // 這裏說是判斷鍵的現有映射
            // 結束上面判斷的時候最終可以獲取到e的值的只有上面兩個判斷中的值。一個是往數組中方結果,一個是往數中方結果
            // 放入的時候有可能存在值了 e就不會爲null
            if (e != null) { // existing mapping for key
            	// 獲取到老值
                V oldValue = e.value;
                // 如果值爲空 或者是不存在
                if (!onlyIfAbsent || oldValue == null)
                	// 就把對應位置的值進行替換
                    e.value = value;
                // 進行一個後置訪問 最終的這個後置訪問是自擴展的,這個擴展在TreeNode繼承的LinkedHashMap中進行了實現,這個操作就先不多說了
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 這字段表示Hash在結構上被修改的次數,是個全局的變量
        // 這個結構修改值的是HashMap的修改以及其內部的修改,它的作用是
        // 在集合視圖上實現迭代器
        ++modCount;
        // size 記錄的是 HashMap的大小,這裏需要注意的是數組的大小還是裏面的結構的大小
        // 在上面的操作中並沒有那個地方來修改這個值,而這個值唯一被增加的地方就是這裏,所以說
        // size 記錄的是內部元素的總共進入了多少個,而不是內部的table的大小
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }




































  


    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

  上面提到了一個size的概念,有如下的一個問題來,下面這個代碼最後輸出的結果是多少

  HashMap<Integer,Object> hashMap = new HashMap<>();
  for (int i = 0; i <100 ; i++) {
      hashMap.put(i,i);
  }
  System.out.println(hashMap.size());
  // 100 
  


  

resize()

  接上面resize()方法 從上面進入到重新設置大小的操作。

   final Node<K,V>[] resize() {
   		//記錄一下原來的table,這裏不管有沒有值都記錄一下。
        Node<K,V>[] oldTab = table;
        // 如果舊錶爲空或者不爲空那麼就需要記錄一下器長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 這裏簡單的解釋一下這個值,capacity * load factor
        // 源碼中的英文翻譯過來是  要調整大小的下一個大小值(容量*負載係數)。
        // 有人會問這個東西值在什麼地方 具體是通過 static final int tableSizeFor(int cap) 操作算出           
        int oldThr = threshold;
        
        int newCap, newThr = 0;
        // 這裏表示 這個值 表如果有值
        if (oldCap > 0) {
        	// 則 看看是否是 比這個 1<<30 大
            if (oldCap >= MAXIMUM_CAPACITY) {
            	// 如果不是則返回就是一個原來的表也就是說沒有辦法再操作了,threshold也調成 最大的整數值
                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 和newThr的時候就使用默認的初始化大小,
            // DEFAULT_INITIAL_CAPACITY = 1<<4
            // DEFAULT_LOAD_FACTOR = 0.75         
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 在上面的判斷結束之後這裏做了一個newThr 爲0 的判斷
        // 這裏如何理解 什麼時候這個值會是0
        // 經過分析可以看到,這個值在初始化的時候爲零,也就是說沒有經過上面的兩個判斷直接進入到了這裏,從代碼邏輯上來看,上面的判斷是對oldCap 和 oldThr的判斷。從這裏可以看出 下面這個IF判斷應該是初始化的邏輯
        if (newThr == 0) {
        	// 上面可以看到如果沒有舊數據,那麼newCap 值應該是默認值16。loadFactor的值應該是 默認值 0.75 從而可以知道 ft的值應該是 12.0f
            float ft = (float)newCap * loadFactor;
            // 計算出一個新的 newThr
            // 我們知道MAXIMUM_CAPACITY 值是 1<<30,ft 經過計算應該是 12.0f,
            // 也就是說看看newCap 和 ft的值與 1<<30  的大小。最終的結果應該是 ft ,如果超過對應的大小就是Int類型的最大值。注意這裏的大小全部是對於數組來做的
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 將臨界區 改爲新的臨界區,這裏是個人叫法,也就是說如果數據超過這個值就一定會發生 hash衝突。
        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) {
                    // 這裏將老數組中的第j個位置的元素給設置爲空
                    oldTab[j] = null;
                    if (e.next == null)
                        // 判斷是否有鏈表存在 如果不存在,則進行e.hash & (newCap - 1) // 給整個元素給定一個新的位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        // 如果 是樹的節點 進行了一個split的操作,整個函數代碼有點多,放到下面分析
                        ((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 {
                        	// 首先 將 e 節點的 下一個節點值獲取到
                            next = e.next;
                            // 第二步 將e 節點的 hash值與table的長度 做 與運算,假設 10 和 12 做與運算 1010 和 1100 1000 也就是說 存在兩個1 纔是1 否則都是0
                            // 這個判斷的意思是 如果 e元素的hash值 和 老的 table的長度做與運算爲0 那麼什麼時候這操作會爲零呢? 就是這個oldCap爲零的時候,那麼什麼時候oldCap爲零,或者說這個oldCap爲零會帶來什麼樣的後果。
                            if ((e.hash & oldCap) == 0) {
                            	// 判斷 尾節點是否爲空如果不爲空則就是它本身是頭節點,如果不是則尾節點的將e節點放入到下一個節點中。
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 如果上面的條件不滿足,一般都是有值的不會爲零
                            else {
                            	// 將原來的數據進行記錄保存到hiTail中,原則與上面的操作原則是一樣的。
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //遍歷完成之後
                        // 如果loTail 不爲空
                        // 也就是說原來的數據 裏面有數據,並且爲其加入了新的數據,並且將所有的數據都放到 數組的第j個節點所在的位置對應的內容中
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 如果歷史有數據,則將數據放入到 j+oldCap 這個位置上,爲了解決節點hash衝突而存在的一個機制,這裏是放到for循環中所以說這個是解決Hash衝突的一個問題。
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        // 最終返回一個新的數組。而這個數組會根據具體的條件產生 ,繼續返回到putVal()方法中
        return newTab;
    }


























treeifyBin()

  進入到節點到樹的操作

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
     // 說明傳入的值是tab 數組 和 hash
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 判斷tab是否爲空 或者是 長度是否小於MIN_TREEIFY_CAPACITY 這個值是 64 ,爲什麼會有這個判斷?鏈表轉換紅黑樹跟tab的 的長度沒有關係,爲什麼會有這樣一個判斷?帶着這個問題繼續往下分析
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        	// 調用了一個擴容操作,其實這個地方就是爲了使用其中的一個split方法。因爲此時的鏈表還沒有達到那個程度,想象一個極端的場景,通過上面的分析我們知道了,key值不重複,在一定程度上可以避免hash衝突,假設從1到100000來設置key值,會出現什麼結果。假設而已。下面就來分析。在上面的分析中知道 resize()方法中再對 擴容操作的時候其實將數組中存入的都是鏈表的頭節點。如果上面這個情況出現的話就會出現一種情況。GC圖如下
            resize();
        // 如果上面條件不滿足 也就是說 兩個條件都不滿足。
        // 這裏的 n 代表的是長度 做了減一操作 然後與hash做對比。最後看是否在存在。
        // 理解一下,如果該位置沒有值 就不處理了以爲看到這個條件結束之後沒有其他的處理邏輯
        // 第二點,到了這裏一般拿到的位置都是有值的,那麼這個值如何處理   
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                // 替換爲樹節點,可見上面的分析中 p 爲啥變成TreeNode是有原因的。
                TreeNode<K,V> p = replacementTreeNode(e, null);
                // 這裏tl是做什麼用的,還記的,在擴展的時候loTail 的作用麼,功能類似就是爲了記錄一下本來的值
                if (tl == null)
                	//  將轉換後的值進行賦值
                    hd = p;
                else {
                	// 原來的值也加入到其中。
                    p.prev = tl;
                    // 將 p 節點連接到tl的下一個節點。
                    tl.next = p;
                }
                // 最終將 p 節點還給原來的節點
                tl = p;
                // 上面這個操作成立的條件就是 e 存在下一個節點,也就是將整個鏈表做轉換操作
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
            	//會看到上面的所有的轉換做成功之後,進行的就是構建的操作將tab上的index位置的設置爲hd,整個的tab放入到了 treeify的方法中
                hd.treeify(tab);
        }
    }












  測試程序代碼如下。

  HashMap<Integer,Object> hashMap = new HashMap<>();
  for (int i = 0; i <100000000 ; i++) {
     hashMap.put(i,i);
  }
  System.out.println(hashMap.size());

  上面場景的GC圖

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述
  停止之後實際物理機內存變化使用變化。
在這裏插入圖片描述
  通過上面的分析可以知道,根據上面的分析可以知道如果在key不存在hash衝突的時候就不會啓動鏈表,也就沒有具體的鏈表到紅黑樹的轉換過程。從而導致了這個現象。那麼根據我們對於hash衝突的理解,就是當這個tab中的key出現 n-1&hash 值相同的時候就是hash衝突。那麼就來看看下面這種情況
  當我們把下面代碼循環次數從1 到100000000不斷擴大的時候,在7個零的時候都沒有問題,在8個零的時候就有問題了。那麼這個問題到底出現在table還內部的TreeNode或者是鏈表呢。

  HashMap<Object,Object> hashMap = new HashMap<>();
  for (int i = 0; i <100000000 ; i++) {
     hashMap.put("s"+i,i);
  }
  System.out.println(hashMap.size());


在這裏插入圖片描述

在這裏插入圖片描述
  分析問題癥結所在其實問題還是出現在這個HashCode上,上面的操作在HashCode上是一個規律性的增長操作並沒有一個合適的操作組合出現key不同的情況,而在判斷的時候我們看到一個判斷
(k = p.key) == key || (key != null && key.equals(k))) 這個判斷 最終的意義我們也瞭解就是判斷hashcode 以及key的值是否相同。這裏我們測試如下的一個代碼,這段代碼的神奇之處就在於它的這個組合保證了s和d兩個字符串的hashCode 是一樣的,但是字符串本身是不一樣的。這個是根據String類型對於HashCode重寫推算出來的。當然我們可以根據這個思路來實現一個新的內容。

    String s ="Ea";

    String d ="FB";

    HashMap<String,Integer> hashMap= new HashMap<>();
    hashMap.put(s,1);
    hashMap.put(d,2);
    System.out.println(hashMap.size());


  根據上面的啓示構建瞭如下的測試代碼

public class Test {
    private static int hash = 0;
    public static void main(String[] args) {
        HashMap<String,Integer> hashMap= new HashMap<>();
        for (int i = 0; i < 100000000 ; i++) {
            String s1 = generateShortUUID();
            String s2 = generateShortUUID();
            int hash1 = hashCode(s1.toCharArray());
            int hash2 = hashCode(s2.toCharArray());
            if (hash1 == hash2) {
                hashMap.put(s1, 1);
            }
        }
        System.out.println(hashMap.size());
    }
    public static int  hashCode(char[] value) {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    public static String[] chars = new String[] { "a", "b", "c", "d", "e", "f",
            "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
            "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
            "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
            "W", "X", "Y", "Z" };
    public static String generateShortUUID() {
        StringBuffer shortBuffer = new StringBuffer();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        for (int i = 0; i < 8; i++) {
            String str = uuid.substring(i * 4, i * 4 + 4);
            int x = Integer.parseInt(str, 16);
            shortBuffer.append(chars[x % 0x3E]);
        }
        return shortBuffer.toString();
    }
    


  在之前線性擴展的過程中7個零的速度是非常快的並且最後打印出來的結果值是10000000,但是使用了上面這種方式的時候,當擴展增加到7個零的時候比之前的耗時要嚴重。
7個零
在這裏插入圖片描述
  在8個零的時候也沒有像之前的哪個樣子出現一個整個的老年代被直接佔滿的情況。而是一個線性增加的過程。
8個零
在這裏插入圖片描述

treeify()

  /**
         * Forms tree of the nodes linked from this node.
         * @return root of tree
         */
        final void treeify(Node<K,V>[] tab) {
        	// 傳入的是數組,輸出結果是一個tree
            TreeNode<K,V> root = null;
            // 首先來初始化 拿到的樹是不是當前需要操作的樹也就是上面的hd,以及next節點 x 表示hd
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
            	// 既然是這樣通過上面的方法知道了hd中存的全是TreeNode節點
            	// 到這裏可以知道其實就是節點鏈表到樹結構的轉換 那麼tab在什麼地方使用呢?
            	// 就是爲了定位在數組中那些節點需要進行操作。
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                // 左右子樹 ,以及根節點的操作。
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                	// 否則 就進行操作了
                	// 首先拿到的key 並且獲取到k的hash值
                    K k = x.key;
                    int h = x.hash;
                    // k了類對象
                    Class<?> kc = null;
                    // 進入死循環這裏需要注意循環結束的條件
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        
                        K pk = p.key;
                        // 這裏的h是x的hash值,ph則是當前節點的hash值,也就是說這個數據的轉換其實是一個hash 值的比較
                        if ((ph = p.hash) > h)
                        	// 這個操作主要是用來做紅黑樹平衡用的。
                            dir = -1;
                        else if (ph < h)
                        	
                            dir = 1;
                        // 進行 判斷比較
                        //  kc == null  Kc爲空 && 判斷也就是說這個條件與第二個條件需要全部滿足。
                        // (kc = comparableClassFor(k)) == null)
                        // 第三個條件與第一個條件和第二個條件的產生的結果做 || 運算
                        // (dir = compareComparables(kc, k, pk)) == 0
                        // 這裏條件滿足之後進行的操作就是 tieBreakOrder(k, pk);
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                                 
                            dir = tieBreakOrder(k, pk);
						// 最終,進入到下面這個邏輯
                        TreeNode<K,V> xp = p;
                        // 進行遍歷 將 拿到的x全部放入到構建好的樹裏面 條件不成立的時候進行一個平衡操作
                        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;
                        }
                    }
                }
            }
            // 進行一個轉換 這個時候用到了 tab
            moveRootToFront(tab, root);
        }










moveRootToFront()

  移動根到頭

/**
         * Ensures that the given root is the first node of its bin.
         */
        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            // 傳入的參數是數組和紅黑樹
            if (root != null && tab != null && (n = tab.length) > 0) {
            	// 條件成立 找到索引位置 這裏使用的是根節點的hash值。n還是數組的長度。
                int index = (n - 1) & root.hash;
                // 將索引位置的 Node 強轉爲 TreeNode 
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                // 判斷 根節點是否與first節點相同,那麼什麼時候回進行如此的操作呢?這個函數的功能就是將這個根節點移動到 數組的前面。從這個地方看似乎是有這樣的一個規律,就是這個數組中其實存在兩類數據,依賴是TreeNode,一類是Node數據。是這樣麼?繼續分析。
                if (root != first) {
                    Node<K,V> rn;
                    // 將root 放到了 tab的index位置
                    tab[index] = root;
                    //將根 的記錄操作節點 rp 而prev的含義就是 TreeNode 的臨時連接存在                    TreeNode<K,V> rp = root.prev;
                    // 這裏到這裏有一個大膽的猜測,就是TreeNode不但有紅黑樹的特點還有鏈表的特點。是這樣麼?
                    if ((rn = root.next) != null)
                    	// 判斷了root的next 這個時候有點難以理解的就是root爲什麼會有next並且不爲空的判斷? 
                    	// 這裏做的操作就是把 root.next.pre指針指向了 root.pre 所指向的內容
                        ((TreeNode<K,V>)rn).prev = rp;
                    if (rp != null)
                    	//如果 rp不爲空 則將rp.next 指向rn
                        rp.next = rn;
                    if (first != null)
                    	// 如果 first不爲空 將 frist.prev 指向 root
                        first.prev = root;
                    // 最終轉換完成之後,進行 root.next = first 的操作 這裏我們知道first是通過根節點的hash值計算出來的鏈表中的位置。其整個作用就是將紅黑樹調整的數組的合適位置上。  
                    root.next = first;
                    root.prev = null;
                }
                assert checkInvariants(root);
            }
        }









總結

  從源碼的閱讀中,可以看到其實我們都不是真的懂HashMap的真的消耗性能在什麼地方。如果我們真的懂了,應該是合理的組合,讓這個HashMap性能用到極致。什麼是極致。就是由一萬個長度的HashMap,table的長度如果是一萬的話,至少我們可以高效利用讓其存儲一百萬的數據。而不是研究第一個元素怎麼把它放入。當然我們要了解第一個元素的放入,而不是糾結第一個元素怎麼放入。這個是第一點。

  第二點,我們真的瞭解table的解決衝突的機制麼?難道真的就是數組加鏈表或者是數組加紅黑樹麼?有沒有更加優化的地方等待我們探索呢?其實這三個點我們都只是略懂皮毛,來說你精通Java,來寫個JVM出來。寫不出來。保證一個HashMap都寫不出來。怎麼在用到極致的過程中產生出自己的方法論,到底這個HashMap怎麼使用才能更高效。或者像是LinkHashMap一樣可不可以繼承HashMap實現自己的擴展高效Map。這個研究纔有意義。

  第三點,面試官自已以爲自己瞭解到夠深刻,其實不然,這個世界上知識很多,其實沒有必要找一個航天工程師來給你打掃衛生。航天工程師其實還有他其他的作用。

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