Java原始碼閱讀之TreeMap(紅黑樹) - JDK1.8

weixin_34050427發表於2018-09-13

閱讀優秀的原始碼是提升程式設計技巧的重要手段之一。
如有不對的地方,歡迎指正~
轉載請註明出處https://blog.lzoro.com

前言

開門見山,山外有山,山外有山...

先簡單介紹下TreeMap,來看下類關係圖。

5548926-0457141b51bd318e.png
image

怎麼說呢,TreeMap就是一個有序的鍵值對集合(這介紹有夠簡單的)。

TreeMap實現了NavigableMap介面, 而NavigableMap則是通過sortedMap間接繼承了Map介面,它定義了一系列導航方法,這些Map之外的方法算是和HashMap的不同,另外的不同點還在於順序性。

關於TreeMapHashMap的異同點,在接下來的每個章節都可能會提到。

如果還未了解過HashMap的,可以移步這裡Java原始碼閱讀之HashMap - JDK1.8和這裡Java原始碼閱讀之紅黑樹在HashMap中的應用 - JDK1.8

接下來,請坐好,準備發車了。

基礎

老規矩,不想上來就整一大堆複雜晦澀的方法,還是先從變數了解起。

成員變數

/**
 * comparator用來保持treemap的順序性
 * 如果是null,則採取自然順序
 *
 * @serial
 */
private final Comparator<? super K> comparator;

/**
 * 紅黑樹根節點
 */
private transient Entry<K,V> root;

/**
 * 鍵值對數量
 */
private transient int size = 0;

/**
 * 結構修改次數
 */
private transient int modCount = 0;

從變數可以簡單看出treemapHashMap有點類似,而不同點在於

  • HashMap
    1、基於雜湊桶+連結串列/紅黑樹實現
    2、無序的
  • TreeMap
    1、基於紅黑樹實現
    2、有序的,通過指定的comparator或者自然順序

接下來看下建構函式

建構函式

/**
 * 空參構造,利用自然排序構造一個空的tree map
 * 所有的key,必須實現Comparable介面
 * 與此同時,所以的key必須具備可比性,{@code k1.compareTo(k2)}不能丟擲{@code ClassCastException}
 * 假如你試圖放一個違反約束的key到map裡面,如:放一個string型別的key到原先儲存interger型別key的map裡面,將會丟擲{@code 
 */
public TreeMap() {
    comparator = null;
}

/**
 * 根據給定的comparator構造一個空的/新的map
 * 所有插入到map的key通過comparator比較器必須具備可比性
 *(因為提供了comparator比較器,所以key可以不用實現Comparable介面)
 * 
 *
 * @param comparator comparator如果為null,則使用自然順序
 */
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}


/**
 * 根據給定個的map和key的自然順序構造一個空的treemap
 * 
 * 關於key的約束同上。
 *
 * 方法的時間複雜度為n*log(n)
 *
 * @param  m 要放到treemap中的map
 * @throws ClassCastException key不具備可比/排序性則拋此異常
 * @throws NullPointerException 指定的map是null則拋NPE
 */
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    //呼叫putAll存放m,後續分析
    putAll(m);
}

/**
 * 根據給定是sortedMap,利用相同的排序方式構造一個新的treemap
 * 
 * 方法以限行時間執行
 *
 * @param  m sortedmap
 * @throws NullPointerException 指定的map是null則拋NPE
 */
public TreeMap(SortedMap<K, ? extends V> m) {
    //獲取sortedmap的comparator
    comparator = m.comparator();
    try {
        //呼叫buildFromSorted來存放m
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

看完了上面幾個建構函式,讓人印象比較深刻的是對於key的約束說明

不指定comparator時,存放到map裡的key必須實現Comparable介面

這裡約束目的就是為了利用可比性來維護treemap的順序性。

上面建構函式中putAllbuildFromSorted沒有跟進做具體分析,放置在功能方法裡一併介紹。

紅黑樹

看完變數和建構函式,本來想直接分析功能方法,但是仔細一看,雖然TreeMap裡紅黑樹的程式碼跟HashMap本質上是一樣的,但是程式碼的結構還是有較大區別,所以先拿來來賞析。(我覺得TreeMap的紅黑樹程式碼可讀性比HashMap來的高多了)

節點定義

依然是利用一個靜態內部類來定義樹節點,這裡跟HashMap中的定義類似,還是比較淺顯易懂,不做太多分析。

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    /**
     * 根據給定的key/value/parent建立一個新的單元節點(黑)
     * 子樹為null
     * 
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * 返回key
     *
     * @return the key
     */
    public K getKey() {
        return key;
    }

    /**
     * 返回跟key關聯的value
     *
     * @return the value associated with the key
     */
    public V getValue() {
        return value;
    }

    /**
     * 替換跟key關聯的value
     *
     * @return 返回舊值
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    /**
     * 比較
     */
    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    /**
     * hashcode
     */
    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    /**
     * toString
     */
    public String toString() {
        return key + "=" + value;
    }
}

左旋

這裡的左旋跟HashMap還是比較相近的,不同點在於HashMap的入參多了一個root來用以指向根節點,而在TreeMap中,root是一個成員變數。

private void rotateLeft(Entry<K,V> p) {
    //null節點忽略
    if (p != null) {
        //取出p的右子樹
        Entry<K,V> r = p.right;
        //用r的左子樹替換p的右子樹
        p.right = r.left;
        //如果r的左子樹存在的話
        //則將r的左子樹的父節點指向p
        if (r.left != null)
            r.left.parent = p;
        //r的父節點指向p的父節點
        //實質上,就是r替換了p的位置
        r.parent = p.parent;
        //如果p節點不存在父節點
        if (p.parent == null)
            //那麼替換了p節點後的r就是根節點
            root = r;
        else if (p.parent.left == p)
            //如果p的父節點存在且p是左子樹
            //則將替換p後的r設定為左子樹
            p.parent.left = r;
        else
            //否則設定為右子樹
            p.parent.right = r;
        //p變成r的左子樹
        r.left = p;
        //修改引用
        p.parent = r;
    }
}

右旋

private void rotateRight(Entry<K,V> p) {
    //null節點忽略
    if (p != null) {
        //取出p的左子樹l
        Entry<K,V> l = p.left;
        //用l的右子樹替換p的左子樹
        p.left = l.right;
        //如果l人右子樹存在
        //則將l的右子樹的父節點指向p
        if (l.right != null) l.right.parent = p;
        //交換l和p的位置
        l.parent = p.parent;
        //如果p的父節點不存在
        if (p.parent == null)
            //那麼替換了p節點後的l就是根節點
            root = l;
        else if (p.parent.right == p)
            //如果p的父節點存在,且p是原右子樹
            //則將替換p後的l設定為右子樹
            p.parent.right = l;
        //否則設定為左子樹
        else p.parent.left = l;
        //修改引用
        l.right = p;
        p.parent = l;
    }
}

插入平衡

插入平衡方法的實現就是我所說的,我覺得比HashMap可讀性強的方法。TreeMap把節點的關係操作封裝成獨立方法了,比如獲取父節點、左子樹、右子樹等,會讓含義很清晰,如果類似於HashMap是通過引用方式的話,很容易原始碼看著看著就暈乎乎了。

/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
    //新節點都為紅色
    x.color = RED;
    //x存在且c不是根節點且x的父節點為紅色
    while (x != null && x != root && x.parent.color == RED) {
        //如果x的父節點是祖父節點的左子樹的話
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //取出祖父節點的右子樹
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //判斷祖父節點右子樹是否為紅色
            if (colorOf(y) == RED) {
                //紅色
                //將父節點變成黑色
                setColor(parentOf(x), BLACK);
                //祖父節點的右子樹變成黑色
                setColor(y, BLACK);
                //祖父節點變成紅色
                setColor(parentOf(parentOf(x)), RED);
                //將x的引用指向祖父節點
                x = parentOf(parentOf(x));
            } else {
                //祖父節點右子樹為黑色
                //x節點是父節點的右子樹
                if (x == rightOf(parentOf(x))) {
                    //x引用指向父節點
                    x = parentOf(x);
                    //左旋
                    rotateLeft(x);
                }
                //將x的父節點變成黑色
                setColor(parentOf(x), BLACK);
                //x的祖父節點變成紅色
                setColor(parentOf(parentOf(x)), RED);
                //右旋
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //如果x的父節點是祖父節點的右子樹的話
            //取出祖父節點的左子樹
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //祖父節點左子樹為紅色
            if (colorOf(y) == RED) {
                //相關變色操作
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //祖父節點左子樹為紅色
                //入股x是父節點的左子樹
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    //右旋
                    rotateRight(x);
                }
                //相關變色操作
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //左旋
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

刪除平衡

刪除平衡也是類似的,程式碼書寫比較規範,為了凸顯我懶,就不新增註釋了,把程式碼貼出來,有緣人自行參悟。

/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

羅列TreeMap的紅黑樹相關程式碼,是想說明TreeMap裡面的實現比起HashMap可讀性更為強一些,但是其實質都是一樣的,所以上面關於插入平衡和刪除平衡的過程這裡不再細說,之前格子的Java原始碼閱讀之紅黑樹在HashMap中的應用 - JDK1.8這篇部落格裡面有過步驟的相關描述,也有一些圖解,有興趣的可以瞭解一下。

功能方法

接下來看下相關功能方法,看下我們平時所使用的方法內部是怎麼實現的。

put

將指定的鍵值對存放到TreeMap,不同於HashMap將元素通過HashCode分散到雜湊桶裡面,TreeMap是通過比較器/自然順序的形式將元素存放到紅黑樹中來保證有序性。

下面開始分析put方法。

/**
 * 存放指定的鍵值對
 * 如果指定的key存在,舊的value將會被新的替換
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 *
 * @return 舊的value值{@code key}, 如果之前不存在,則返回null
 *         (返回null也有可能key對應的值是null)
 * @throws ClassCastException 指定的key不具備可比性的話則拋此異常
 * @throws NullPointerException 使用自然排序時指定的key為null/comparator不允許null的key,則拋NPE
 *
 */
public V put(K key, V value) {
    //根節點
    Entry<K,V> t = root;
    //如果根節點還不存在(TreeMap是空的)
    if (t == null) {
        //這裡的比較做一個型別檢查
        //可能null
        compare(key, key); // type (and possibly null) check
        //初始化一個節點
        root = new Entry<>(key, value, null);
        //size + 1
        size = 1;
        //修改計數 + 1
        modCount++;
        //返回null
        return null;
    }
    //如果TreeMap不為空
    //定義比較值
    int cmp;
    //定義父節點
    Entry<K,V> parent;
    // 分離Comparator和比較路徑
    Comparator<? super K> cpr = comparator;
    //如果存在Comparator
    if (cpr != null) {
        //通過迴圈找到合適的節點
        //通過二叉查詢樹的性質進行查詢
        //知道找到合適的節點
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                //如果找到相同的key,則替換值後返回
                return t.setValue(value);
        } while (t != null);
    }
    //不存在比較器,則採用自然順序比較
    else {
        //自然順序比較不允許key為null
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        //同樣採用迴圈來查詢插入位置
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    //新建節點
    Entry<K,V> e = new Entry<>(key, value, parent);
    //根據比較結果,來決定將節點放置在左邊還是右邊
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    //插入平衡
    fixAfterInsertion(e);
    //size + 1
    size++;
    //修改計數 + 1
    modCount++;
    //返回null(執行到這一步,證明未找到相同的key,如果有,則在上面就return了)
    return null;
}

看完了put的,再把之前建構函式中的未加分析的putAll一併閱讀(完全無違和感)。

/**
 * 將指定map中的元素都存放到當前treemap
 *
 * @param  map map
 * @throws ClassCastException key不合法(參照建構函式章節)
 * @throws NullPointerException 指定的map為null/或者存在null的key且treemap不允許null-key的情況下丟擲NPE
 */
public void putAll(Map<? extends K, ? extends V> map) {
    //獲取大小
    int mapSize = map.size();
    //treemap為空且指定的map不為空並且map是可排序的map
    if (size==0 && mapSize!=0 && map instanceof SortedMap) {
        //獲取Comparator
        Comparator<?> c = ((SortedMap<?,?>)map).comparator();
        //判斷Comparator是否跟當前的一致
        if (c == comparator || (c != null && c.equals(comparator))) {
            //操作計數 + 1
            ++modCount;
            try {
                //呼叫buildFromSorted進行處理
                buildFromSorted(mapSize, map.entrySet().iterator(),
                                null, null);
            } catch (java.io.IOException cannotHappen) {
            } catch (ClassNotFoundException cannotHappen) {
            }
            return;
        }
    }
    //呼叫父類的putAll
    super.putAll(map);
}

通過分析以上的程式碼,可以看出putAll裡面的邏輯還是比較簡單的,一是判斷當前treemap是否為空,且給定map的大小合法,並且是給定的map是SortedMap的例項if (size==0 && mapSize!=0 && map instanceof SortedMap)
如果是,則取出比較器判斷後呼叫buildFromSorted進行處理
如果不是,則呼叫父類的putAll進行處理。

這裡留有兩個疑問,buildFromSorted和父類的putAll究竟做了哪些處理來完成集合元素的存放呢?

下面一步步分析,先從父類的putAll看起 。

/**
 * {@inheritDoc}
 * 嗯,懶得翻譯了,反正我翻譯水平也比較差。
 *
 * @implSpec
 * This implementation iterates over the specified map's
 * <tt>entrySet()</tt> collection, and calls this map's <tt>put</tt>
 * operation once for each entry returned by the iteration.
 *
 * <p>Note that this implementation throws an
 * <tt>UnsupportedOperationException</tt> if this map does not support
 * the <tt>put</tt> operation and the specified map is nonempty.
 *
 * @throws UnsupportedOperationException {@inheritDoc}
 * @throws ClassCastException            {@inheritDoc}
 * @throws NullPointerException          {@inheritDoc}
 * @throws IllegalArgumentException      {@inheritDoc}
 */
public void putAll(Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
}

是不是一目瞭然了。如果上面提到的判斷if (size==0 && mapSize!=0 && map instanceof SortedMap)不成立,則呼叫父類的putAll方法:通過迴圈,將元素一個個放到treemap當中,這裡的放置put就是在本章節開頭分析的put方法。

那麼,還剩下一個疑問,如果上面的判斷成立,buildFromSorted又做了哪些操作呢?

/**
 *
 * 線性時間的樹構造演算法(根據排序資料)
 * 可以從迭代器/流當中接受鍵值對
 * 有很多方法入參,但是似乎還是比其他選擇更好(PS:我也不知道其他選擇是什麼)
 *
 * 該方法接受的4種格式說明:
 *
 *    1) Map.Entries迭代器.  (it != null, defaultVal == null).
 *    2) key的迭代器.        (it != null, defaultVal != null).
 *    3) 交替序列化的鍵值對流.(it == null, defaultVal == null).
 *    4) 序列化的鍵流. (it == null, defaultVal != null).
 *
 * 假設呼叫此方法前comparator已經被設定
 *
 * @param size 鍵/或者鍵值對的數量
 * @param it  不為null的話, 新的entries通過這個迭代器建立
 * @param str 不為null的話, 新的entries通過序列化流來建立
 *        準確點說,it和str必須一個不為null
 * @param defaultVal 不為null的話, 會作為預設值
 * @throws java.io.IOException 讀取流時可能會丟擲NPE,如果str為null則不會發生這種情況
 * @throws ClassNotFoundException 讀取物件是可能拋此異常.如果str為null則不會發生這種情況
 */
private void buildFromSorted(int size, Iterator<?> it,
                             java.io.ObjectInputStream str,
                             V defaultVal)
    throws  java.io.IOException, ClassNotFoundException {
    //設定size
    this.size = size;
    //呼叫buildFromSorted來確定root
    //分割線之後繼續分析
    //這裡有個小插曲computeRedLevel
    //computeRedLevel是根據節點數量來計算完全二叉樹的層級
    //其實從名字看來,可以理解為計算紅色節點的層級
    root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                           it, str, defaultVal);
}

---------------------------------------------------

/**
 * 計算紅色節點所在層級
 * (完全二叉樹的層級)
 * 從0開始
 *
 */
private static int computeRedLevel(int sz) {
    int level = 0;
    for (int m = sz - 1; m >= 0; m = m / 2 - 1)
        level++;
    return level;
}


---------------------------------------------------
//個是buildFromSorted的實際實現方法

/**
 * 遞迴的、真正的實現方法(之前是幫助方法). 
 * 跟之前的方法比較,相同的引數命名具有相同的意義
 
 * 增加的引數說明在下方
 * 
 * 假定在呼叫此方法之前已經設定了樹圖的比較器和大小欄位。(它忽略了這兩個欄位)。
 *
 * @param level 當前樹的層級. 初始化呼叫應該為0.
 * @param lo 子樹的首個節點索引. 初始化應該為0.
 * @param hi 子樹的尾節點索引. 初始化應該為size - 1
 * @param redLevel 節點該為紅色的層級,必須以size和computeRedLevel計算出來的相等
 */
@SuppressWarnings("unchecked")
private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
                                         int redLevel,
                                         Iterator<?> it,
                                         java.io.ObjectInputStream str,
                                         V defaultVal)
    throws  java.io.IOException, ClassNotFoundException {
    /*
     * 策略: 根節點是最接近中間節點的元素. 為了得到它,首先我們必須遞迴構建完整的左子樹,以便抓取所有的元素
     * 然後我們可以繼續處理右子樹
     *
     * lo和li引數是為當前子樹提取迭代器/流的最小和最大指標,
     * 它們實際上沒有索引,我們只是按順序處理,確保items被按相應的順序處理。
     * 
     */

    //如果hi小於lo,
    if (hi < lo) return null;

    //mid=(lo+hi)/2; - 無符號右移
    int mid = (lo + hi) >>> 1;
    
    //左子樹
    Entry<K,V> left  = null;
    //如果lo小於mid
    //遞迴構造左子樹
    if (lo < mid)
        left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                               it, str, defaultVal);

    // extract key and/or value from iterator or stream
    //從迭代器/流中獲取鍵值對
    K key;
    V value;
    //使用迭代器
    if (it != null) {
        //沒有有預設值
        if (defaultVal==null) {
            Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
            key = (K)entry.getKey();
            value = (V)entry.getValue();
        } else {
            //有預設值
            key = (K)it.next();
            value = defaultVal;
        }
    } else { // use stream
        //使用流
        key = (K) str.readObject();
        value = (defaultVal != null ? defaultVal : (V) str.readObject());
    }
    //建立節點
    Entry<K,V> middle =  new Entry<>(key, value, null);

    // color nodes in non-full bottommost level red
    //非null節點且是紅色層級的,染色成紅色
    if (level == redLevel)
        middle.color = RED;
    //判斷左子樹是否為null
    if (left != null) {
        //指向左子樹
        middle.left = left;
        //修改引用
        left.parent = middle;
    }
    //遞迴構造右子樹
    if (mid < hi) {
    
        Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                           it, str, defaultVal);
        middle.right = right;
        right.parent = middle;
    }
    //到遞迴的最外層的話這裡的middle就是最終的根節點
    return middle;
}
    

到這裡,關於TreeMapput相關方法就分析完畢了,有幾個要點梳理一下

  • 1、put方法根據比較器/自然順序將元素放置到紅黑樹特定位置後,進行插入平衡
  • 2、putAll實際上有兩種情況,一個是迭代取出元素呼叫父類的put,另外是呼叫buildFromSorted完成TreeMap構造
  • 3、呼叫buildFromSorted的前提是,入參必須是SortedMap的例項(還有其他限制,詳見上面的if條件)
  • 4、buildFromSorted裡面有一個computeRedLevel,是用來計算紅色節點層級(也可以理解為計算完全二叉樹層級)
  • 5、實際實現buildFromSorted的方法,是一個遞迴呼叫的過程,通過middle,遞迴構造左右子樹來完成整棵樹的構建。

Go On,下面是remove的方法。

remove

/**
 * 如果存在的話,根據指定的key從treemap中移除指定的鍵值對
 *
 * @param  key 要移除的鍵值對的key
 * @return 和{@code key}相關聯的舊值
 *         如果{@code key}.沒有對映的話為{@code null},返回null的時候也有可能和key相關聯的是null
 * @throws ClassCastException 指定的key無法和map中的key進行比較,則拋此異常
 * @throws NullPointerException 指定的key是null且該treemap採取自然排序/comparator不允許null的key時,拋NPE
 */
public V remove(Object key) {
    //根據key獲取指定元素節點
    Entry<K,V> p = getEntry(key);
    //為null則返回
    if (p == null)
        return null;
    //取出舊節點的值
    V oldValue = p.value;
    //刪除元素
    deleteEntry(p);
    //返回舊值
    return oldValue;
}

從原始碼可以看出,remove方法體裡面有兩個關鍵呼叫,getEntrydeleteEntry,深入瞭解一下。

/**
 * 根據給定給定key,返回元素,如果沒存在,則返回null
 *
 * @throws ClassCastException 指定的key無法與map中的比較時,丟擲此異常
 * @throws NullPointerException 指定的key是null且該treemap採取自然排序/comparator不允許null的key時,拋NPE
 */
final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    // 判斷是否存在comparator
    if (comparator != null)
        //如果comparator存在的話,呼叫getEntryUsingComparator
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    //利用二叉樹性質,進行迴圈搜尋
    while (p != null) {
        //自然比較
        int cmp = k.compareTo(p.key);
        //根據比較結果,決定取左子樹還是右子樹
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            //如果比較結果相等,則返回該元素
            return p;
    }
    return null;
}

//繼續看getEntryUsingComparator方法

/**
 * 通過comparator獲取元素的版本.從genEntry分離出來(整潔美觀效能beautiful~)
 * (對於大多數方法來說它是不值得的,因為大多數方法較少依賴於比較器效能,但是在這裡它就是醬紫的呀,它是值得的)
 */
final Entry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
        K k = (K) key;
    //取出比較器
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        Entry<K,V> p = root;
        //從根節點迴圈
        while (p != null) {
            //通過比較器獲取比較結果
            int cmp = cpr.compare(k, p.key);
            //根據比較結果,決定取左子樹還是右子樹
            //嗯,其他的就跟上面自然順序處理是一樣樣兒的
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

查詢元素的方法還是比較簡單易懂的,但是不能漏掉deleteEntry這個查詢後刪除的方法,其實這裡的deleteEntry就是紅黑樹的節點刪除操作了,之前也貌似也分析過,這裡還是把程式碼和註釋貼來,也許你跟我一樣也是小懶蛋呢(科普一下:優秀的懶人會有創新的,因為不想重複勞動)

5548926-2558bc0ed126cf5b.png
/**
 * 刪除p節點,然後處理刪除平衡
 */
private void deleteEntry(Entry<K,V> p) {
    //首先,現實相關計數處理
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    //內部嚴謹的話,拷貝p的後置節點給p,然後將p指向後置節點
    //左右子樹都存在的情況
    if (p.left != null && p.right != null) {
        //獲取後置節點
        Entry<K,V> s = successor(p);
        //後置節點的相關值賦值給p
        p.key = s.key;
        p.value = s.value;
        //p指向s
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //開始在替換節點進行修正
    //如果p的左右子樹都存在一個的話,則p在上面的條件分支裡已經指向s了
    //取出替換節點
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    //判斷替換節點是否存在
    if (replacement != null) {
        // Link replacement to parent
        //修改父節點引用
        replacement.parent = p.parent;
        //如果p不存在父節點,那麼替換p的replacement節點就是根節點了
        //很好,登基了(朕一日不死,你們就都是太子)
        if (p.parent == null)
            root = replacement;
        //如果p是父節點的左子樹
        else if (p == p.parent.left)
            //那麼修改父節點的左子樹引用為新的替換節點
            p.parent.left  = replacement;
        else
            //否則修改右子樹引用
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        //p節點的相關引用置為null,以便後面的刪除平衡處理
        p.left = p.right = p.parent = null;

        // Fix replacement
        //如果p節點是黑色節點的話,則進行刪除平衡
        if (p.color == BLACK)
            fixAfterDeletion(replacement);//這個方法在開頭的紅黑樹說明有,或者可以參考我的另外一篇hashmap紅黑樹部落格
    //如果替換節點不存在,且p的父節點也不存在
    } else if (p.parent == null) { // return if we are the only node.
        //則證明p的唯一的節點,返回null
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        //p有父節點,但是沒有子節點了
        //判斷p的顏色是否為黑
        if (p.color == BLACK)
            //如果是,進行刪除平衡
            fixAfterDeletion(p);
        //p的父節點存在
        //判斷p是父節點的左子樹還是右子樹
        //並進行相關引用修改
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

//獲取後置節點

/**
 * 返回後置節點如果存在的話,如果不存在,返回null
 */
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    //t為null,則直接返回null
    if (t == null)
        return null;
    //右子樹存在
    else if (t.right != null) {
        //取出右子樹
        Entry<K,V> p = t.right;
        //迴圈,遍歷並迴圈取左子樹,取出最後一個
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        //左子樹存在
        //取出父節點
        Entry<K,V> p = t.parent;
        //ch指向t
        Entry<K,V> ch = t;
        //現在的ch(t)是p的子樹
        
        //迴圈(只要父節點存在,且ch(t)節點是父節點的右子樹的話)
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
//可以看出來,取出後置節點是這麼處理的:
1、如果t的右子樹存在的話,就一路向左下遍歷,直到null
2、如果t的左子樹存在的話,就一路向向上遍歷(t必須是父節點的右子樹),直到不符合情況

get

(⊙o⊙)…

如果仔細看了remove章節的話,其實這個章節可以略過了。

因為get屬於門面方法,實際實現也是由getEntry提供的。


public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

containsKey/containsValue

判斷TreeMap是否存在對應的key或者對應的value。

判斷key比較簡單,跟上面get章節是相同道理的,根據key去獲取元素,並判斷元素是否為null。

判斷value跟判斷key不一樣,但是邏輯也很清晰,首先取出首個元素,然後迴圈迭代,用指定value和每個元素的value做比較,相同則返回。



public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

--------------------------------------------------

public boolean containsValue(Object value) {
    //迭代判斷
    for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
        if (valEquals(value, e.value))
            return true;
    return false;
}

forEach

迴圈迭代,並對每個元素做指定的操作(action)。

這裡的迴圈迭代跟上面的containsValue是一樣的,不通點在於containsValue是對每個元素執行判斷,而forEach是對每個元素執行相應的action。

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    int expectedModCount = modCount;
    for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
        action.accept(e.key, e.value);

        if (expectedModCount != modCount) {
            throw new ConcurrentModificationException();
        }
    }
}

entrySet

本來不打算把這個拿出來分析的,因為完整的分析篇幅實在是太長了。

但是既然都這麼長了,還在乎差這一截嗎~

這個方法我們用的也是相對比較頻繁的,單看entrySet方法根本沒什麼好看的,很簡單,內部有一個entrySet變數,如果未初始化,則new一個,如果已初始化,則返回。

public Set<Map.Entry<K,V>> entrySet() {
    EntrySet es = entrySet;
    return (es != null) ? es : (entrySet = new EntrySet());
}

來看一下EntrySet的資料結構,它是TreeMap的內部類,並且繼承了AbstractSet,並實現了相關方法,用過Set的小夥伴應該相熟悉。

// TreeMap.java 1057行

//Entry定義
class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    /**
     * 返回迭代器
     */
    public Iterator<Map.Entry<K,V>> iterator() {
        //這裡呼叫的getFirstEntry方法,之前分析過
        //獲取了首節點元素後,建立一個EntryIterator
        //這裡迭代器相關程式碼不貼了,有興趣的可以自行了解
        //TreeMap 1238行
        return new EntryIterator(getFirstEntry());
    }

    /**
     * 判斷是否包含元素o
     */
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
        Object value = entry.getValue();
        Entry<K,V> p = getEntry(entry.getKey());
        return p != null && valEquals(p.getValue(), value);
    }

    /**
     * 移除元素o
     */
    public boolean remove(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
        Object value = entry.getValue();
        Entry<K,V> p = getEntry(entry.getKey());
        if (p != null && valEquals(p.getValue(), value)) {
            deleteEntry(p);
            return true;
        }
        return false;
    }

    public int size() {
        return TreeMap.this.size();
    }

    public void clear() {
        TreeMap.this.clear();
    }

    public Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<K,V>(TreeMap.this, null, null, 0, -1, 0);
    }
}

單從上面的原始碼看來,entrySet其實也沒什麼好分析的,不過Set裡面還是有很多方法在平時會用到的,之後找個時間,專門開一篇分析Set好了。

天色已晚,各位小夥伴下車洗洗睡吧。

總結

嗯,沒有總結,都在上面了。

5548926-d4a7fa23da4bce48.png
image

溜了溜了。給個讚唄。

相關文章