透過源碼看本質——1、HashMap

概述

HashMap 作爲平時開發過程中常見的數據結構經常被用到,網上關於它的博文已經有非常多,爲了加深我對它的理解,我計劃就 JDK1.8 版本,通過源碼的形式整理下它的原理。


HashMap

HashMap在源碼中是這樣定義的:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable,Serializable

這裏我通過簡單類圖描述一下這幾個類之間的關係:
在這裏插入圖片描述

  • Map 接口主要聲明一些常用的 key-value 接口方法

  • AbstractMap 抽象類實現 Map 接口,它實現了部分接口方法,以及創建靜態內部類 SimpleEntry

  • Cloneable 接口主要和 clone() 方法有關,實現該接口可以重寫 clone() 方法

  • Serializable 主要和序列化有關,實現該接口可以讓對象序列換

關於 SimpleEntry 類這裏我們先不詳細說明,等後面用到了我們再說


構造方法

HashMap 類的構造方法主要有以下幾種:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

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;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

其中前三種構造方法比較常見,第四種構造方法用的比較少,這裏我們主要以常用的前三種爲主。

通過源碼我們可以看出,前三種構造方法主要是爲了初始化屬性 loadFactorthreshold 的值。這裏我直接給出結論:

  • loadFactor:hashMap 的負載因子,當集合中元素個數達到負載因子所對應數量後 hashMap 會 擴容,通過源碼可以看出默認的負載因子是0.75

  • threshold:hashMap 所對應管道數,每個管道可以保存多個元素

下面我們看一下 threshold 屬性是如何計算出來的,即 tableSizeFor() 方法的源碼:

static final int MAXIMUM_CAPACITY = 1 << 30;

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;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

通過該方法可以獲取大於等於參數的最小的2的冪次方:也就是說,如果參數是5,輸出8,參數是9,輸出16,每次都輸出大於當前參數的最小二次冪。

從這裏也就可以看出,hashMap 的管道個數總是2的冪次方,關於這樣做的原因後面根據源碼具體分析。下面我們看一個草圖,通過這個草圖對 hashMap 有一個大致的認識:
hashMap工作原理


put() 方法

有了上面的鋪墊,下面我們具體看一下 hashmap 是如何保存一個元素的:

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
	// 暫時先省略,下面着重解決
}

這裏我先給出 putVal() 方法這五個參數所代表的意義:

  • hash:key 的 hash 值
  • key :要保存的 key
  • value:要保存的 value
  • onlyIfAbsent:是否修改已經存在的數據,如果該參數爲 true,則不會修改已經存在的key
  • evict:該參數在 hashMap 中沒有用到,在 hashMap 的子類 linkedHashMap 有用到,如果該參數爲true,在添加元素後可能會刪除頂部元素。

接下來我們看一個 hash() 方法的實現:

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

該方法只是將 key 的 hashCode() 值散列的高位向地位移動一下。


Node內部類

在正式開始閱讀 putVal() 源碼前,我們先看看內部類 Node 的源碼,該類是 putVal() 方法的基礎:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
	
	// 省略 get() set()
    
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

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

通過源碼我們可以看出,Node 內部保存 key、value 、hash 值以及指向下一個節點的 next 引用。其中它重寫了 hashCode() 方法 和 equals() 方法。關於爲啥重寫這兩個方法可以點擊這裏參考我之前的博客。

其實看到這裏 hashMap 的原理基本上已經可以猜出來了,只需要把上面的草圖改成下面這樣:

HashMap原理
也就是說,hashMap 實際上就是通過 Node 內部類組成的數組和鏈表實現的


putVal() 源碼

有了上面這些基礎,我們具體來看 putVal() 方法具體是怎麼做的。在下面源碼中我儘量通過註釋的形式介紹,部分特別重要的內容附加在源碼後面:

// transient 表示該屬性不會序列化,這裏table就表示上圖中的數組塊內容
transient Node<K,V>[] table;

// 單個鏈表的閾值
static final int TREEIFY_THRESHOLD = 8;

// 數組中工作節點個數
transient int modCount;

// 當前hashMap存儲的節點個數
transient int size;

// hashMap 所能容納的最大節點個數
int threshold;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 說明數組還沒有初始化,即 hashmap 第一次添加元素時
    if ((tab = table) == null || (n = tab.length) == 0)
    	// resize()方法初始化數組,關於該方法的源碼下面我會給出,這裏的n表示數組的長度,可以理解爲hashMap的管道數
        n = (tab = resize()).length;
    // 判斷管道頭是否爲空,如果爲空,直接將要put的元素添加到數組對應下標
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 判斷數組元素key是否等於要put的元素key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 判斷是否使用TreeNode
        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) {
                    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))))
                    break;
                p = e;
            }
        }
        if (e != null) {
        	// 這裏的 e可能是新添加的Node節點,也可能是老的Node節點(hash相等,key相等情況)
            V oldValue = e.value;
            // 更新新值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 該方法在hashMap爲空,什麼都沒做
            afterNodeAccess(e);
            // 返回給節點對應的老值
            return oldValue;
        }
    }
    // 工作節點個數加一
    ++modCount;
    // 判斷hashmap元素數是否大於閾值
    if (++size > threshold)
    	// 擴容
        resize();
    // 該方法在 hashmap 爲空
    afterNodeInsertion(evict);
    return null;
}

上述代碼中,我主要提一下這行代碼:

p = tab[(n - 1) & hash]

從這裏我們可以看出,元素屬於哪個數組節點是由它的hash值和數組長度決定的。元素的hash值是隨機的,數組的長度是確定的,爲了讓元素儘可能平均的分配到所有節點,(n-1) & hash 的計算結果必須儘可能的均勻。

當元素的長度總是2的冪次方時,n-1的值轉化爲二進制總是111…1。在這種情況下,隨機的hash值計算出的結果也就相對比較均勻,這也是爲什麼 hashmap 中數組的長度總是2的冪次方的主要原因。

  • 爲什麼要讓元素分配的相對均勻呢?

    元素在數組上分配均勻無論是添加還是修改或是查詢,都只需要遍歷較少的節點數量,這對於效率的提升至關重要。


hashmap 的初始化及擴容

在整理 putval() 方法時,我們提到 hashmap 是在第一次put元素時初始化,當元素數量超過閾值時,擴容也是調用該方法,下面我整理一下 resize() 方法的源碼:


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 原先的數組大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 原先hashmap所能容納的最大元素數
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	// 數組已經達到最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
        	// 將最大元素數設置爲 MAX
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 數組長度擴容爲原來的兩倍,如果hashmap最大容量超過16,也變爲原來的兩倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    // 如果數組長度爲空,最大元素數不爲0,將數組長度設置爲容量大小
    else if (oldThr > 0)
        newCap = oldThr;
   	// hashmap還未初始化時,此時容量以及數組大小都爲0
    else { 
    	// 默認數組大小爲16
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 默認hashmap閾值爲 16 * 0.75(負載因子)
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果此時hashmap容量爲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 = newTab;
    // 如果是數組擴容,重新分配所有元素
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 數組節點只有一個元素時的情況
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
              	// 存的是TreeNode時的情況
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 循環重新計算所有節點屬於哪個下標
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

上述代碼中,我主要提一下 hashmap 擴容時元素重新分配的原理:

  • 如果某個數組節點對應的鏈表只有一個元素,直接根據新的數組長度計算

  • 如果某個數組節點對應的鏈表有多個元素,則會根據一個 奇妙的算法計算

首先,hashmap 每次擴容時,數組長度都會變爲原來的 2倍。轉化爲二進制就可以這樣表示:
數組長度二進制
而計算元素屬於哪個數組下標是和數組長度減1來計算的,數組長度減1轉二進制分別對應:

  • 原來的數組長度轉二進制:N 個 “1”
  • 現在的數組長度轉二進制:N + 1 個 “1”

也就是說,通過新數組長度所計算出的下標,其實就是多算了一位第 n+1 位的 “1”,舉個例子:

hash值等於10的元素轉二進制 -> 1010,將它保存到長度爲2的數組
此時屬於哪個下標 -> 1010 & 0001,也就是說只看最後一位
如果數組長度擴容爲原來的二倍
此時屬於哪個小標 -> 1010 & 0011,也就是說,在原來的基礎上,往前多判斷一位
寫成公式:擴容後的下標 = 擴容前下標 + 新加位是否爲1
爲了判斷這個新加位是否爲1,只需要讓hash值直接和原數組長度進行&運算即可,
也就是上面代碼所對應的 e.hash & oldCap
這個新加長度所對應的值,實際上也就是 oldCap

總結一下,數組擴容後,當前元素要麼屬於原來的數組下標,要麼屬於原來的數組下標加上原數組長度


Node 轉 TreeNode

在整理 putVal() 方法時,當單個鏈表元素數量超過鏈表閾值時會調用 treeifyBin() 方法,在正式學習treeifyBin() 方法源碼前,我們先看看內部類 TreeNode 的結構:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
	
	// 省略部分實現好的內部方法
}

從該對象的屬性很明顯可以看出,它是一種樹形結果,並且是性能較好的 紅黑樹。而 LinkedHashMap.Entry 又是 Node 的子類,也就是說,該TreeNode 也是 Node 的子類,通過這種方式實現了 Node 轉 TreeNode 的可能性,因爲對象可以 向上轉型


下面我們具體看 treeifyBin() 方法的源碼:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 默認情況下只會初始化
    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 {
        	// Node 轉 TreeNode
            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);
    }
}

該方法是在某個鏈表超過閾值時,將所有Node節點轉換爲TreeNode節點,並通過前驅、後綴引用連接起來,下面我們主要看一下 treeify() 方法的源碼:

final void treeify(Node<K,V>[] tab) {
	// 創建頭節點
    TreeNode<K,V> root = null;
    // 循環當前鏈表節點
    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;
            // 根據 hash 值向左向右遍歷
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                // 如果hash值相等,根據comparable接口方法判斷是否相等
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
				// 記錄當前節點,表示要判斷節點的父節點
                TreeNode<K,V> xp = p;
                // 只有進入該方法,才表示找的真正的落點,進行保存
                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);
}

該方法表示當某個鏈表長度超過閾值時,將鏈表轉換爲紅黑樹的結構,提高效率。關於紅黑樹的結構我們後面做專門介紹(內容實在太多),就不再這裏深入方法本身做探討了。


get() 方法

看過put()方法的源碼, get() 方法就簡單了很多,下面我們直接看源碼:

public V get(Object key) {
   Node<K,V> e;
   return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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

看過 put() 方法源碼後,看 get() 方法簡直不要太輕鬆,幾乎沒有什麼新內容,這裏我們簡單描述一下如果鏈表結構爲 TreeNode 紅黑樹時,如何遍歷。具體我們看代碼:

final TreeNode<K,V> getTreeNode(int h, Object k) {
	// 這裏主要保證每次遍歷總是從最高級節點開始,root() 方法只是循環遍歷到父節點
    return ((parent != null) ? root() : this).find(h, k, null);
}

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        // 向左遍歷
        if ((ph = p.hash) > h)
            p = pl;
       	// 向右遍歷
        else if (ph < h)
            p = pr;
        // hash 相等且 key 相等,說明就是當前節點
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        // hash 相等,但是左節點爲空時,向右遍歷
        else if (pl == null)
            p = pr;
       	// hash 相等,但是右節點爲空時,向左遍歷
        else if (pr == null)
            p = pl;
        // kc爲 true 時,根據 compare 方法判斷,默認爲false
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        // 根據左節點查詢
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        // 根據右節點查詢
        else
            p = pl;
    } while (p != null);
    return null;
}

總結一下:當鏈表轉換爲紅黑樹結構後,通過hash值作爲參考,key值作爲一錘定音的判斷來實現的。當hash值相等時,根據對象的 compare() 方法做判斷


putTreeVal() 方法

有了上面紅黑樹的基礎,我們再來看看,當鏈表結構轉化爲紅黑樹結構後,如何添加新元素。具體我們看源碼:

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    // 定位到最頂級父節點
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (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) {
            // compare() 方法相同只會遍歷一次
            if (!searched) {
                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;
            }
            // 通過 compareTo 方法和 identityHashCode() 確定方向
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            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)
                ((TreeNode<K,V>)xpn).prev = x;
            // 將重新平衡後的紅黑樹的頭結點設置在數組中
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

從源碼可以看出,當鏈表轉換爲紅黑樹後,添加元素的邏輯實際上和轉化時大致相同。


紅黑樹的擴容

上面我們講了鏈表的擴容方法,這裏我們主要看看當鏈表轉換爲紅黑樹後,整個紅黑樹的擴容方法,即 split() 方法的源碼:

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;
    // 遍歷整個紅黑樹,將整個紅黑樹改爲兩個鏈表,一個鏈表記錄新增位 & 爲0,表示擴容後還在當前下標,另一個記錄新增位 & 爲1,表示擴容後再原下標+老數組長度下標
    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) {
    	// 如果長度小於6,TreeNode 轉爲 Node
        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);
        }
    }
}

總結以下:紅黑樹的擴容就是遍歷整個紅黑樹,將結果轉爲兩個鏈表,一個鏈表記錄下標會變的情況,另一個鏈表記錄下標不會變的情況,根據鏈表的長度,決定是否將鏈表轉Node鏈表,還是紅黑樹化。


紅黑樹的平衡

在鏈表處理轉紅黑樹或給紅黑樹添加新元素後,都會執行 balanceInsertion() 方法平衡紅黑樹,下面我們來看一下它的源碼:

// root 表示紅黑樹的根節點,x表示最後一個添加的節點
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    	// 表示 x 就是root節點,直接返回
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 如果x的父節點就是黑色,並且它沒有父節點,直接返回root,表示只有兩個節點,root就是根節點
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 如果x的父節點是它自身父節點的左子樹
        if (xp == (xppl = xpp.left)) {
        	// x 的父節點的父節點的右子樹不爲空
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // x 的父節點的父節點的右子樹爲空
            else {
            	// 判斷 x 是否它父節點的右子樹
                if (x == xp.right) {
                	// 向左旋轉x的父節點
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        // 向右旋轉
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 如果是右子樹,下述情況和上面對稱
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

上述代碼本身比較複雜,主要涉及紅黑樹的左旋、右旋以及顏色的處理。關於這塊我暫時也看的迷迷糊糊,等後續整理紅黑樹時着重整理,我暫時先給出兩個關於左旋以及右旋的示意圖:

在這裏插入圖片描述
如上圖所示,就可以讓樹變得相對比較平衡。關於紅黑樹的只是暫且整理到這裏,後面加上顏色着重分析。


關於 hashmap 的源碼我就整理到這裏,對於一些其他方法的實現,我想如果你讀懂了 put() 以及 get() 方法,那基本不會有啥難度。

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