全網!最全!最詳細! HashMap 源碼解析

要思考的問題

  • HashMap的底層數據結構(節點結構,這種結構有什麼優點)
  • 如何處理hash衝突
  • 怎麼擴容?擴展機制是什麼?
  • 增刪改查過程
  • 鏈表到紅黑樹的轉換過程,反之?
  • 紅黑樹相關(見另一篇數據結構之紅黑樹)
  • hash計算

達到的目標

  • 掌握底層數據結構
  • 掌握擴容原理
  • 掌握hash衝突的處理過程
  • 掌握增刪改查過程

看之前要掌握的知識點

紅黑樹

看之前大體瞭解的知識點

hash算法

Poisson分佈

poisson分佈

開始

HashMap的繼承體系

[外鏈圖片轉存失敗(img-z4jA6cu0-1567747065188)(./images/HashMap01-繼承體系.png)]

  • AbstractMap: map的抽象類,以最大限度的減少實現Map接口的類的工作量。

hashMap結構

字段解釋

常量字段(默認值字段)

  • DEFAULT_INITIAL_CAPACITY=1<<4: 默認的初始容量,默認是爲16,必須是2的n次方.爲什麼呢? 見擴容的方法。
  • DEFAULT_LOAD_FACTOR=0.75f: 默認的負載因子。它和哈希表的容量的乘積是決定是否重新hash的閾值。
  • TREEIFY_THRESHOLD=8: 使用樹而不是鏈表的計數閾值。當桶的元素添加到具有至少這麼多節點時,桶被轉換爲樹。
  • UNTREEIFY_THRESHOLD=6: 用於在調整大小操作期間解除(拆分)桶的桶計數閾值。(untreeifying不是一個英語單詞,這裏的以是非樹化,即轉換成普通列表的過程).也就是說從樹轉換成普通的桶(鏈表)的閾值。
  • MAXIMUM_CAPACITY=1<<30: 最大的容量: 1<<30,如果具有參數的任一構造函數隱式指定更高的值,則使用此參數。必須是2的n次方,小於等於1<<30
  • MIN_TREEIFY_CAPACITY=64: 容器可以樹化的最小容量(否則,如果bin中的節點太多,則會調整表的大小.)應該至少爲 4 * TREEIFY_THRESHOLD,以避免調整大小和樹化閾值之間的衝突.

類屬性

  • table: transient HashMap.Node<K,V>[] table; table在首次使用時初始化,並根據需要調整大小。分配時,長度始終是2的冪。(我們還在一些操作中容忍長度爲零,以允許當前不需要的自舉機制)
  • entrySet: transient Set<Map.Entry<K,V>> entrySet; 保存緩存的entrySet.
  • size: transient int size; map中元素的數量。結構修改是那些改變HashMap中映射數量或以其他方式修改其內部結構(例如,rehash)的修改。此字段用於在HashMap的Collection-views上快速生成迭代器(見ConcurrentModificationException)

注意: 這些字段都是 transient 的? 爲什麼呢?

  • loadFactor: final float loadFactor;hash表的負載因子,在實例化hashTable的時候指定,該對象內不能變更(final);
  • threshold: int threshold;, 下一次調整容器大小的閾值. threshold=capacity * load factor

HashMap的兩種節點

  • 基本的哈希桶的節點(鏈表的結點) Node

static class Node<K,V> implements Map.Entry<K,V>它繼承了Map的Entry,是對子類的行爲規範。要求提供了getKey(),getValue()等常用方法。

鏈表節點的結構如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 避免重複計算key的hash值
    final K key;
    V value;
    // 指向下一個節點的指針
    HashMap.Node<K,V> next;

    Node(int hash, K key, V value, HashMap.Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    @Override
    public final K getKey()        { return key; }
    @Override
    public final V getValue()      { return value; }
    @Override
    public final String toString() { return key + "=" + value; }

    // todo 沒有找到在哪裏使用了這個方法
    @Override
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    @Override
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
  • Tree的節點 TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>繼承了其子類的Entry, 子類的Entry繼承了父類的Node.注意了,這裏乍一看還挺亂。來張圖吧。
[外鏈圖片轉存失敗(img-ZPV5rtwT-1567747065189)(./images/hashMap的節點的繼承圖.png)]

這裏呢,TreeNode其實是Node的孫子, 也就是說HashMap的樹節點是鏈表節點的孫子輩兒的。
爲什麼要使兩種節點有繼承關係呢? 爲什麼TreeNode不直接繼承Node節點呢?

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    HashMap.TreeNode<K,V> parent;  // red-black tree links
    HashMap.TreeNode<K,V> left;
    HashMap.TreeNode<K,V> right;
    HashMap.TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, HashMap.Node<K,V> next) {
        super(hash, key, val, next);
    }
    // 省略其他代碼
}

HashMap增加方法 HashMap#put()

/**
*  將指定的value和key關聯在map中。
*  如果map中已經存在了key,那麼將會替換掉老的value。
* @param key key 指定的key
* @param value value 和指定key關聯的value
* @return 如果返回了value,就說明map中原來和key關聯是有值的。如果返回null就說明沒有value。
*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

這裏就比較有看點了, 1.這裏是hashMap的增加方法,增加方法裏必然會遇到hash衝突的問題,我們等會看下hash衝突是如何處理的,還會涉及到擴容的問題, 我們也要來看看他是怎麼擴容的, 擴容的過程中還會遇到普通的桶轉換成樹的過程.我們先來看下hash值是怎麼計算出來的。

  • hash值的計算 {TODO 和jdk1.7中的比較}
/**
 * 計算key的hashCode並且和hashCode值高16位進行異或運算。(異或: 相同爲0,不同爲1)
 * 混和低位和高位,就是爲了加大低位的隨機性,而且混合後的低位摻雜了高位的部分特徵,
 * 這樣高位的信息也被變相的保留了下來。
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

爲什麼這麼做呢? 見HashMap的Hash函數到底有什麼意義

  • 那我們接下接着看putVal()方法。
/**
 * 實現Map.put相關的方法。
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 *                     如果是true的,不會修改存在的值。返回老的值。
 * @param evict if false, the table is in creation mode.
 *              如果爲false的時候,表屬於創建模式,第一次新增元素的時候。
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {

    HashMap.Node<K,V>[] tab;
    HashMap.Node<K,V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        // 如果數組爲null,或者數組長度爲0的時候,數組需要調整大小。
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 定位到數組的桶爲null的時候,創建桶內的第一個元素。next=null;
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果桶不爲null,則創建鏈表
        HashMap.Node<K,V> e; K k;
        // p表示當前桶的第一個元素。
        // 如果新增的元素和第一個元素相等的話(出現hash衝突),暫存已經存在的元素到變量e中。
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof HashMap.TreeNode)
            // 如果是樹節點。
            e = ((HashMap.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);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        // 如果桶內的元素數量達到樹化的閾值,將鏈表轉換成樹。
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    // 如果第一個元素和要新增的元素hash,key都相等的話,直接進行新增操作。
                    break;
                p = e;
            }
        }

        if (e != null) { // existing mapping for key
            // 如果原來的元素不爲空,保留原來的值。
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                // 覆蓋掉原來的value;
                e.value = value;
            // 留一個無方法體的方法,供子類擴展
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // failFast計數
    ++modCount;
    if (++size > threshold)
        // 如果table中的桶的數量超過了閾值。擴容。
        resize();
    // 供子類擴展的方法。
    afterNodeInsertion(evict);
    return null;
}

這段代碼裏中有三處重要的地方,resize(),treeifyBin(),putTreeNode(),接下來我們依次看下這三個方法。

resize

/**
 * 初始化,或者加倍表格的大小
 * 如果爲null時候,根據字段threshold的初始容量進行分配
 * 否則,因爲我們正在使用二次冪擴展,所以每個bin中的元素必須保持相同的索引,或者在新表中以兩個偏移的冪移動
 *
 * @return the table 新的表
 */
final HashMap.Node<K, V>[] resize() {
    HashMap.Node<K, V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 如果舊錶的大小大於0
        if (oldCap >= MAXIMUM_CAPACITY) {
            // hash表達到最大容量
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY) {
            // 如果翻倍後舊錶大小<最大表長度,並且舊錶長度>默認初始化長度。
            // 擴容的閾值也翻倍。 還是等級 table.length*loadFactor
            newThr = oldThr << 1; // double threshold
        }
    } else if (oldThr > 0) { // initial capacity was placed in threshold
        // 舊錶長度<=0,舊的threshold>0,
        // 就把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) {
        // 如果新的擴縮容閾值等於0,設置新的擴縮容閾值爲新的容量*負載因子.
        float ft = (float) newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 重新創建新的hash表
    @SuppressWarnings({"rawtypes", "unchecked"})
    HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];
    table = newTab;
    // 如果舊錶不爲空,進行擴容.
    // 否則(舊錶爲空)就進行初始化過程.
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            HashMap.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 HashMap.TreeNode) {
                    // 如果當前桶是棵紅黑樹
                    ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                } else { // preserve order
                    // 桶是鏈表,將該桶內的元素重新分配到表中。

                    HashMap.Node<K, V> loHead = null, loTail = null;
                    HashMap.Node<K, V> hiHead = null, hiTail = null;
                    HashMap.Node<K, V> next;

                    // 遍歷桶內的元素,將元素重新分配到hash表內的各個桶中。
                    // 具體的實現過程是: 將當前的元素的hash值和容量取&,如果>0,那就說明該元素應該分配到新的桶內。
                    // 桶的位置就是: oldCap+j.即桶原來容器+該元素所在的桶的下標。(hiHead所標識的位置)
                    // 反之如果hash值是==0的,那麼該元素就應該還在當前桶內。(loHead所標識的位置)
                    // 這裏所說的位置都是指桶的下標,整個表都是新的了,位置肯定都變了。
                    // 爲什麼可以這麼實現呢?
                    // 因爲擴容的時候,使用的是原來容量的2倍進行擴容的。所以就可以使用(oldCap+j)的方式來確定元素的新位置了。
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            // 還在原桶中
                            if (loTail == null)
                                loHead = e;
                            else {
                                // 位置最後一個節點爲空,使用e=next的時候,next爲null的情況。
                                // 在桶內元素遍歷完成後,會把桶的最後一個元素的next置爲null。
                                loTail.next = e;
                            }
                            loTail = e;
                        } else {
                            // 放置到新的桶內。
                            if (hiTail == null)
                                hiHead = e;
                            else {
                                // 位置最後一個節點爲空,使用e=next的時候,next爲null的情況。
                                // 在桶內元素遍歷完成後,會把桶的最後一個元素的next置爲null。
                                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;
}

看一個散列還算非常均勻的例子來看擴容過程。

[外鏈圖片轉存失敗(img-4b59ZB73-1567747065195)(./images/hashMap04-Put方法過程01.png)]

那麼進行擴容的過程是怎麼樣的呢?

[外鏈圖片轉存失敗(img-uOO7peRH-1567747065197)(./images/hashMap05-resize方法01.png)]

以元素1和12爲例,看擴容過程:
元素1的hash值爲49.(以hashMap計算hash值的方式得出。), 與15取&計算桶的下標爲1, 擴容後,與31取&,計算桶的下標爲17.所以擴容前位置是0,擴容後元素1的存放位置是17。
代碼中是怎麼完成這個過程的呢?
和擴容前hash表的容量取&,得 49 & 16 = 16 > 0(代碼第86-96行), 新的桶的頭節點(對應代碼裏的hiHead)就是當前節點1,尾節點(hiTail)賦爲當前節點。然後進行下一次do...while循環,處理節點12, 計算出節點12的hash值爲1569,進行計算1569 & 16 = 0 == 0原來桶的頭結點是節點12,尾節點也是節點12(對應着代碼第76-86行),這樣hitail和loTail均不爲null, 所以然後直接使用newTab[j] = loHead;newTab[j + oldCap] = hiHead;的方式確定桶的位置。這個案例裏,處理完節點12纔會確定桶的位置。因爲原來的表中下標爲1的桶中有兩個元素1和12.那桶裏只有一個元素的怎麼處理的呢?newTab[e.hash & (newCap - 1)] = e; e是當前節點,newCap是新表的容量。

如果你想問爲什麼能使用 hash & olcCap==0?來決定是newTab[j] 還是 newTab[j+oldCap]這種方式來確定新的桶的下標的話。 那麼原因就是擴容使用的是2次冪的方式,容量是原來容量的2倍.所以就可以使用 hash & olcCap==0?來判斷了。

這個例子呢,演示了擴容過程中的鏈表的新增和擴容過程。再回頭看resize方法,還有一種情況我們沒有分析過.那就是

...
else if (e instanceof HashMap.TreeNode) {
    // 如果當前桶是棵紅黑樹
    ((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
}
...
/**
    * 將原來樹桶中的節點拆分爲更低或更高的樹桶,如果太小的話就轉化成鏈表
    * 只被resize方法調用
    *
    * @param map   hash表
    * @param tab   表中的指定的桶的頭結點(桶是一個棵樹)
    * @param index 要拆分的hash表的節點
    * @param bit   the bit of hash to split on 要分裂的hash位
    */
final void split(HashMap<K, V> map, HashMap.Node<K, V>[] tab, int index, int bit) {
    HashMap.TreeNode<K, V> b = this;
    // Relink into lo and hi lists, preserving order
    HashMap.TreeNode<K, V> loHead = null, loTail = null;
    HashMap.TreeNode<K, V> hiHead = null, hiTail = null;
    // lc代表的是原來的桶的元素的數量
    // hc代表新的桶中的元素的數量, 用來和UNTREEIFY_THRESHOLD比較決定是否要轉換結構.
    int lc = 0, hc = 0;
    // 這裏還是當做鏈表去處理,把桶內的元素重新散列。
    for (HashMap.TreeNode<K, V> e = b, next; e != null; e = next) {
        next = (HashMap.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;
        }
    }
    //  散列完後,判斷原來的桶(lo)和新的桶中的元素個數
    //  然後決定轉換爲樹還是鏈表
    if (loHead != null) {
        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);
        }
    }
}

將樹重新穿換成鏈表的過程就比較簡單了:

/**
    * Returns a list of non-TreeNodes replacing those linked from
    * this node.
    */
final HashMap.Node<K, V> untreeify(HashMap<K, V> map) {
    HashMap.Node<K, V> hd = null, tl = null;
    for (HashMap.Node<K, V> q = this; q != null; q = q.next) {
        // replacementNode:將TreeNode轉成Node
        HashMap.Node<K, V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

這裏就是和紅黑樹相關的內容了,這裏關鍵的是split調用了一個treeify的方法。這個方法同時也被treeifyBin調用了.所以treeify方法就和treeifyBin方法一塊分享。
順便提一嘴,他們有如下的關係:

[外鏈圖片轉存失敗(img-YyYNbt4W-1567747065200)(./images/hashMap06-紅黑樹相關方法調用關係.png)]
其中藍色的是紅黑樹的方法,黃色的是HashMap調用的方法。

treeifyBin

/**
* 將鏈表轉換成樹。
* 替換給定hash值的索引處的桶的所有節點,如果表太小(table.length小於64),就調整大小.這裏其實是對hash表的一種優化,防止因爲表長度太小而轉換成樹,造成性能浪費
* @param hash 用於確定桶的位置。
*/
final void treeifyBin(HashMap.Node<K, V>[] tab, int hash) {
    int n, index;
    // 鏈表的節點
    HashMap.Node<K, V> e;
    // 如果hash表爲空或者hash表的長度小於最小化的樹化容量(64),這時會重調整大小。
    // 將容量擴大爲原來的兩倍。
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
        resize();
    } else if ((e = tab[index = (n - 1) & hash]) != null) {
        `HashMap.TreeNode<K, V> hd = null, tl = null;
        do {
            // 構建一個樹的節點。
            HashMap.TreeNode<K, V> p = replacementTreeNode(e, null);
            // 如果尾爲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);
        }`
    }
}

其實呢,這個treeifyBin方法還是做了一些將桶樹化的前置操作,然後將裝有TreeNode節點的桶交給了treeify方法去真正的轉換爲一棵紅黑樹。那我們接下來看下treeify方法。注意這個方法定義在HashMap.TreeNode#treeify()

treeify()方法
/**
 * Forms tree of the nodes linked from this node.
 * 把該節點連接的所有節點組成一棵樹。(樹化的過程)
 */
final void treeify(HashMap.Node<K, V>[] tab) {
    // 該棵樹的根節點。
    HashMap.TreeNode<K, V> root = null;
    // x是遍歷的每個節點。
    for (HashMap.TreeNode<K, V> x = this, next; x != null; x = next) {
        // 存下下一個節點。(指向下一個節點的指針)
        next = (HashMap.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 (HashMap.TreeNode<K, V> p = root; ; ) {
                // dir,負值和0爲左子樹,正值爲右子樹。
                int dir, ph;
                K pk = p.key;

                /*************判斷節點在左子樹還是右子樹 -start***************/
                // h爲當前節點的hash值。
                // p是父節點, ph是父節點的hash值。
                if ((ph = p.hash) > h) {
                    // 放在左子樹
                    dir = -1;
                } else if (ph < h) {
                    // 放在又子樹
                    dir = 1;
                }
                //如果當前節點和父節點的hash值相等:
                //如果節點的key實現了Comparable, 或者 父節點和當前節點的key爲一個。
                else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
                        (dir = compareComparables(kc, k, pk)) == 0) {
                    // k是當前節點的key,pk是父節點的key
                    // 根據hashMap定義的規則,判斷當前節點應該位於左子樹還是右子樹。
                    dir = tieBreakOrder(k, pk);
                }
                /*************判斷節點在左子樹還是右子樹 -end***************/

                HashMap.TreeNode<K, V> xp = p;
                // p==null,代表着遍歷到了葉子節點。
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    // xp是當前節點的父節點。
                    x.parent = xp;
                    if (dir <= 0){
                        xp.left = x;
                    } else {
                        xp.right = x;
                    }
                    // 平衡插入的紅黑樹(完成插入後,紅黑樹的性質可能被破壞,這裏進行重新平衡)
                    root = balanceInsertion(root, x);
                    break;
                }
            }

        }
    }
    //確保紅黑樹的根節點是桶的第一個節點。
    moveRootToFront(tab, root);
}

在這裏呢,有3個方法沒有仔細去說明,分別是 tieBreakOrder(),balanceInsertion() 和 moveRootToFront(tab, root),注意,這三個方法在下面的PutTreeVal中也有調用.當然包括調整平衡的左旋(rotateLeft),右旋(rotateRight)方法.我們接着往下看吧。

balanceInsertion方法

在說這個方法之前,先總結下紅黑樹變換的5條規則。

  • 規則1: 紅黑樹爲空樹 ==> {直接插入當前節點,節點塗爲黑色。}
  • 規則2: 插入節點的父節點是黑色 ==> {直接插入當前節點.}
  • 規則3: 當前節點的父節點是紅色,並且叔叔節點是紅色。==> {父節點塗黑,叔叔節點塗黑,祖父節點塗紅.}
  • 規則4: 當前節點的父節點是紅色,叔叔是黑色,當前節點是父節點的右子樹. ==> {當前節點的父節點作爲新的當前節點,以新的當前節點左旋。}
  • 規則5: 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是父節點的左子樹. ==> {父節點變爲黑色,祖父節點變爲紅色,以祖父節點爲支點右旋.}
    下面結合代碼看HashMap是怎麼實現上面這個5個規則的:
/**
 * 調整紅黑樹
 * @param root 根節點
 * @param x 當前節點
 */
static <K, V> HashMap.TreeNode<K, V> balanceInsertion(HashMap.TreeNode<K, V> root,
                                                        HashMap.TreeNode<K, V> x) {
    x.red = true;
    // xp: 當前節點的父節點(父節點)
    // xpp: 當前節點的父節點的父節點(祖父節點)
    // xppl: 當前節點的父節點的父節點的左子樹(叔叔節點)
    // xppr: 當前節點的父節點的父節點的右子樹(叔叔節點)
    for (HashMap.TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
        // 規則1
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 父節點爲黑色 或者祖父節點爲空==>規則2
        else if (!xp.red || (xpp = xp.parent) == null) {
            return root;
        }

        // 父節點是左子樹
        if (xp == (xppl = xpp.left)) {
            // 父節點是左子樹,且祖父節點存在右子樹(叔叔節點爲右子樹),並且叔叔爲紅色。 ==> 父節點是右子樹時的性質1.
            if ((xppr = xpp.right) != null && xppr.red) {
                // 叔叔節點塗黑
                xppr.red = false;
                // 父節點塗黑
                xp.red = false;
                // 祖父節點塗紅
                xpp.red = true;
                // 以祖父節點爲新的當前節點
                x = xpp;
            }
            // 祖父節點沒有右子樹或者有右子樹,顏色爲黑色。
            else {
                // 當前節點是父節點的右子樹==> 規則4
                if (x == xp.right) {
                    // 左旋
                    root = rotateLeft(root, x = xp);
                    // 設置祖父節點要麼爲空要麼是父節點。
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 規則5
                if (xp != null) {
                    // 父節點塗成黑色
                    // 此時xp可能爲root.
                    xp.red = false;
                    // 如果xp不是root的時候。
                    if (xpp != null) {
                        // 祖父節點塗成紅色,右旋。
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }

        // 父節點不是左子樹==> 父節點是右子樹。
        else {
            // 叔叔節點(祖父節點的左子樹),叔叔爲紅色 ==> 規則3
            if (xppl != null && xppl.red) {
                    // 叔叔塗黑
                xppl.red = false;
                // 父節點塗黑
                xp.red = false;
                // 祖父節點塗紅
                xpp.red = true;
                // 以祖父節點爲新的當前節點
                x = xpp;
            }
            // 祖父節點沒有右子樹或者有右子樹,顏色爲黑色。 ==> 規則4
            else {
                // 當前節點是左子樹
                if (x == xp.left) {
                    // 右旋
                    root = rotateRight(root, x = xp);
                    // 設置祖父節點要麼爲空要麼是父節點。
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // ==> 規則5
                if (xp != null) {
                    xp.red = false;
                    // 如果有祖父
                    if (xpp != null) {
                        // 祖父節點塗成紅色,右旋。
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}
rotateLeft 左旋

這裏的代碼不能用語言描述,真的是隻能意會不能言傳啊。

static <K, V> HashMap.TreeNode<K, V> rotateLeft2(HashMap.TreeNode<K, V> root, HashMap.TreeNode<K, V> p) {
            HashMap.TreeNode<K, V> r, pp, rl;
            // p是父節點
    if (p != null && p.right != null) {
        // 右孩子
        r = p.right;
        // 右孩子有左孩子的話.
        if (r.left != null) {
            // 右孩子變成右孩子的左孩子。即rl變成了p的右孩子。
            p.right = r.left;
            rl = r.left;
            rl.parent = p;
            // 注意此時r沒有關聯。
        }
        pp = p.parent;
        // 如果p沒有有父節點的話。
        if (p.parent == null) {
            // 將r的父節點置爲null
            r.parent = p.parent;
            // 顏色塗成黑色,並且r就是根節點。
            (root = r).red = false;
        }
        //  如果p節點有父節點,並且p是左子樹的話
        else if (pp.left == p) {
            // 將祖父節點的左子樹置爲r,
            pp.left = r;
        } else {
            // 將祖父節點的右子樹置爲r,
            pp.right = r;
        }
        // 將r和p連接起來。
        r.left = p;
        p.parent = r;
    }
    return root;
}

注意下,這裏的代碼是我修改之後,JDK的源碼看起來很精簡,理解起來,嘖嘖嘖。

MD,來張圖:

[外鏈圖片轉存失敗(img-HtJlVEms-1567747065203)(./images/HashMap07-左旋的過程.png)]

這裏假設右孩子是有左孩子的。如果沒有的話,那就直接去掉綠色的rl就好了。

rotateRight

右旋的過程同理:

static <K, V> HashMap.TreeNode<K, V> rotateRight(HashMap.TreeNode<K, V> root,
                                                         HashMap.TreeNode<K, V> p) {
    HashMap.TreeNode<K, V> l, pp, lr;
    if (p != null && (l = p.left) != null) {
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        l.right = p;
        p.parent = l;
    }
    return root;
}

這圖啊,有空再做吧。今天太累了。

還有一個方法:

moveRootToFront
 /**
  * Ensures that the given root is the first node of its bin.
  * // 確保紅黑樹的根節點是桶的第一個節點。
  * 爲什麼不直接將tab[index]==root? 是爲了樹重新轉換成鏈表的時候使用的。
  */
static <K, V> void moveRootToFront(HashMap.Node<K, V>[] tab, HashMap.TreeNode<K, V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index];
        // 判斷第一個節點和root是不是相等的,判斷的是地址。
        if (root != first) {
            HashMap.Node<K, V> rn;
            tab[index] = root;
            HashMap.TreeNode<K, V> rp = root.prev;

            if ((rn = root.next) != null) {
                // root的後一個節點的指向前的指針指向root的前一個節點。
                ((HashMap.TreeNode<K, V>) rn).prev = rp;
            }

            if (rp != null) {
                // root的前一個節點的指向後的指針指向root的後一個節點。
                rp.next = rn;
            }

            if (first != null) {
                // 第一個元素的前指針指向root
                first.prev = root;
            }
            // root的後向指針指向first
            root.next = first;
            // root的前向指針置爲null
            root.prev = null;
        }
        // 遞歸不變檢查
        assert checkInvariants(root);
    }
}

putTreeNode

final HashMap.TreeNode<K, V> putTreeVal(HashMap<K, V> map, HashMap.Node<K, V>[] tab,
                                                int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    HashMap.TreeNode<K, V> root = (parent != null) ? root() : this;
    for (HashMap.TreeNode<K, V> p = root; ; ) {
        int dir, ph;
        K pk;
        /***************判斷 左右子樹 ******************/
        if ((ph = p.hash) > h) {
            dir = -1;
        } else if (ph < h) {
            dir = 1;
        } else if ((pk = p.key) == k || (k != null && k.equals(pk))) {
            return p;
        } else if ((kc == null &&
                (kc = comparableClassFor(k)) == null) ||
                (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                HashMap.TreeNode<K, V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                                (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        /***************判斷 左右子樹 end******************/

        HashMap.TreeNode<K, V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            HashMap.Node<K, V> xpn = xp.next;
            HashMap.TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((HashMap.TreeNode<K, V>) xpn).prev = x;
            // 這裏比較重要了,不過我們在treeify中已經說過了。
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

這樣,HashMap的新增過程我們就處理完了。

HashMap刪除方法 HashMap#remove()

/**
 * 從map中刪除指定的key,如果key存在的話
 * @param key key whose mapping is to be removed from the map
 * @return value 如果key存在,返回key對應的Value,如果不存在返回null
 */
public V remove(Object key) {
    HashMap.Node<K, V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

其中計算hash值的方法還是和之前的一樣。

removeNode

/**
 * Implements Map.remove and related methods.
 * 實現Map.remove相關的方法
 * @param hash       hashCode
 * @param key       key
 * @param value     value
 * @param matchValue 如果是true,僅在value相等的時候刪除。
 * @param movable   如果爲false,則在刪除節點的時候不移動其他節點。
 * @return 返回刪除的節點
 */
final HashMap.Node<K, V> removeNode(int hash, Object key, Object value,
                                    boolean matchValue, boolean movable) {
    HashMap.Node<K, V>[] tab;
    HashMap.Node<K, V> p;
    int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        HashMap.Node<K, V> node = null, e;
        K k;
        V v;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof HashMap.TreeNode) {
                // 找到紅黑樹中的節點
                node = ((HashMap.TreeNode<K, V>) p).getTreeNode(hash, key);
            } else {
                // 刪除鏈表中的節點1: 查找到節點的位置。
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key ||
                                    (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 真正的去刪除的過程。
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof HashMap.TreeNode) {
                // 刪除紅黑樹的節點
                ((HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
            }else if (node == p) {
                // 桶中只有當前的節點。
                tab[index] = node.next;
            }else {
                // 鏈表中節點的刪除
                p.next = node.next;
            }
            // 修改次數+1
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

還有一個最難理解的方法落在了紅黑樹的移除上了。

HashMap#TreeNode#removeTreeNode

還是先看下紅黑樹的刪除是怎麼回事。

在刪除方法調用之前必須要有存在的給定節點。
這比典型的紅黑刪除代碼更混亂,因爲我們不能將內部節點的內容與葉子後繼交換,後者由遍歷期間可獨立訪問的“下一個”指針固定。 所以我們交換樹鏈接。 如果當前樹似乎有太少的節點,則紅黑樹(bin)將轉換回普通的鏈表(普通bin). (測試會在2到6個節點之間觸發,具體取決於樹結構)。
上面是 removeTreeNode方法的解釋.說實話,沒理解…

HashMap的刪除不同於普通的紅黑樹的刪除, 因爲它其中還維護了,一個鏈表的指向. HashMap採用的是將樹中的兩個節點進行換位, 顏色也要進行互換,來保證紅黑樹的平衡,並不改變二者在鏈表中的位置,互換後,刪除節點此時的左子樹是空的,將問題轉換成了對左子樹爲空的節點的刪除。

有一個簡單的問題,千萬不要弄混了,就是TreeNode中要刪除的節點是誰??

刪除的簽名是這樣的:final void removeTreeNode(HashMap<K, V> map, HashMap.Node<K, V>[] tab,boolean movable),並沒有傳 TreeNode啊?是不是??

幹嗎呢!大兄嘚. 要刪除的節點是:this啊。我們現在走到了TreeNode內部了!! 它本身就是要被刪除的節點啊。

好了,那我現在要告訴你: 刪除自己!

HashMap刪除紅黑樹的節點,實際上就是 TreeNode自己刪除自己。那麼它是怎麼刪的呢?

它分成了三步:

  • 1.將刪除節點從雙鏈向鏈表中刪除.
  • 2.將刪除節點與其右子樹最小節點互換,之後平衡樹
  • 3.將樹根節點,移動到tab[index]指針處
final void removeTreeNode(HashMap<K, V> map, HashMap.Node<K, V>[] tab,
                                  boolean movable) {
        // 注意了: 這個時候被刪除的節點是誰??
        // 是this.
        int n;
        if (tab == null || (n = tab.length) == 0)
            return;

        // 找到對應的索引(確定對應桶的位置), n 是當前表的長度
        int index = (n - 1) & hash;
        // first: 第一個樹節點(當前爲父節點),root,父節點。rl:
        HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index], root = first, rl;
        // succ:下一個節點(鏈表的指向)。pred, 前一個節點。
        HashMap.TreeNode<K, V> succ = (HashMap.TreeNode<K, V>) next, pred = prev;

        if (pred == null) {
            // 前一個爲空時,即當前接是父節點:(被刪除的節點是根節點)
            tab[index] = first = succ;
        } else {
            // 否測,前一個節點的下一個執行當前節點的下一個。(意會)
            pred.next = succ;
        }

        if (succ != null) {
            // 當前節點的後節點不爲null,後一個節點的前節點指向當前節點的前節點(意會)
            succ.prev = pred;
        }

        if (first == null) {
            // 如果刪除當前節點,該桶變成了null的。就直接返回
            return;
        }

        if (root.parent != null) {
            // 重置table[index]處爲樹的根節點。
            root = root.root();
        }

        // PS: 說點沒用, JDK除了部分ifelse不加括號之外,
        // 其實換行,還是用的挺多的,看起來也挺舒服的。
        // 值得借鑑
        if (root == null
                || (movable && (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
            // 樹太小了,將樹轉換成鏈表
            tab[index] = first.untreeify(map);  // too small
            return;
        }
        /*****注意!!! 此時已經從雙向鏈表中刪除了, 第一步走完。******/

        // p是待刪除的節點,pl當前節點的左孩子節點,pr當前節點的右孩子節點,replacement,用來交換的節點。
        HashMap.TreeNode<K, V> p = this, pl = left, pr = right, replacement;
        if (pl != null && pr != null) {

            // s爲右子樹的最小的節點,sl爲左子樹(一下五行和源碼略有不同)
            HashMap.TreeNode<K, V> s = pr, sl = s.left;
            while (sl != null) { // find successor
                s = sl;
                sl = s.left;
            }

            // 交換顏色
            boolean c = s.red;
            s.red = p.red;
            p.red = c; // swap colors

            // 交換節點連接
            HashMap.TreeNode<K, V> sr = s.right;
            HashMap.TreeNode<K, V> pp = p.parent;
            // pr是當前節點的右孩子節點
            // s是當前節點的右子樹的最小的節點
            // p的右子樹,只有s這一個節點
            if (s == pr) { // p was s's direct parent
                p.parent = s;
                s.right = p;
            } else { //
                // sp: 最小節點的父節點
                HashMap.TreeNode<K, V> sp = s.parent;
                if ((p.parent = sp) != null) {
                    if (s == sp.left)
                        sp.left = p;
                    else
                        sp.right = p;
                }
                if ((s.right = pr) != null)
                    pr.parent = s;
            }
            // 置null孩子。
            p.left = null;
            if ((p.right = sr) != null) {
                sr.parent = p;
            }
            if ((s.left = pl) != null) {
                pl.parent = s;
            }
            if ((s.parent = pp) == null) {
                root = s;
            } else if (p == pp.left) {
                pp.left = s;
            } else {
                pp.right = s;
            }

            // 確定要交換的節點完畢,交換節點
            if (sr != null) {
                replacement = sr;
            } else {
                replacement = p;
            }
        } else if (pl != null) {
            // 當前樹只含有左子樹
            replacement = pl;
        } else if (pr != null) {
            // 當前樹,只有又子樹
            replacement = pr;
        } else {
            // 無孩子
            replacement = p;
        }

        if (replacement != p) {
            HashMap.TreeNode<K, V> pp = replacement.parent = p.parent;
            if (pp == null)
                root = replacement;
            else if (p == pp.left)
                pp.left = replacement;
            else
                pp.right = replacement;
            p.left = p.right = p.parent = null;
        }

        // 是否要進行重平衡樹?
        HashMap.TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);

        // 在平衡後刪除該節點
        if (replacement == p) {  // detach
            HashMap.TreeNode<K, V> pp = p.parent;
            p.parent = null;
            if (pp != null) {
                if (p == pp.left)
                    pp.left = null;
                else if (p == pp.right)
                    pp.right = null;
            }
        }
        // 參數moveable控制是否刪除節點後確保樹的根節點爲鏈表的頭節點
        if (movable) {
            // 將樹根節點,移動到tab[index]指針處
            moveRootToFront(tab, r);
        }
    }

這樣呢,整個刪除過程就完成了。
用官方中的話,比較混亂。尤其是涉及到紅黑樹的刪除,這部分內容。還是需要好好消化,消化的。

下面我們還剩下兩個內容:修改和查找

HashMap的修改方法

留坑~

HashMap的查找方法

留坑~

問題解答

  • 如果我的HashMap的初始大小設置爲[3|9|12],第一次擴容的時候,容量變爲了多少? 是如何進行擴容的?
  • (有毒的問題)假設Hash表的長度是32,已知某一個bin中的鏈表長度爲7,如果新增一個元素還是在該bin中的時,會進行什麼操作? resize還是treeifyBin? 假設完成這個操作後該bin中元素數量沒變,又新增一個元素還是到該bin中,這時進行什麼操作?

總結

  • 表中允許null的鍵和null值。
  • 線程不同步,
  • 不保證元素的順序。

網上常見面試問題彙總以及參考解答

冷門知識點

  • failFast機制。

JDK變更歷史說明

課後娛樂

  • java實現紅黑樹
  • 自定義實現hashMap。

參考文檔&答謝

感受

  • 註釋: 新字段要加註釋標註此字段的作用,該字段是什麼含義。

閱讀之前記錄

1.圖解。遇到問題,畫圖說明。
2.一定要有自己的理解。
3.對比其他版本JDK。

如果記得還行的話,就關注下 公衆號 呀 ,看最新看詳細的文章!
在這裏插入圖片描述

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