1. 程式人生 > >Java集合詳解--什麼是Map

Java集合詳解--什麼是Map

引言

在很久很久以前,講過Set的實現原理,講到Set就是Map的馬甲,那麼今天我們就來看看Map是如何實現的(本文都基於JDK1.8的版本

什麼是Map


Map和Collection有關的幾個map的關係圖
這裡寫圖片描述

  • Map的定義
java.util
public interface Map<K, V>
An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.
This interface takes the
place of the Dictionary class, which was a totally abstract class rather than an interface. The Map interface provides three collection views, which allow a map's contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in
which the iterators on the map's collection views return their elements. Some map implementations, like the TreeMap class, make specific guarantees as to their order; others, like the HashMap class, do not.
  • Map的三個特點

    1.包含鍵值對
    2.鍵唯一
    3.鍵對應的值唯一

Map還提供了3個集合檢視,分別是一組鍵值對,一組鍵,一組值

  • Map有哪些方法
    這裡寫圖片描述

原始碼分析

  • HashMap

    先來看看常量

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

如果不指定初值的話,列表的長度就為16,預設載入因子為0.75

再來看看成員變數

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;
這個就是列表

/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;
這個是用於快取節點

/**
 * The number of key-value mappings contained in this map.
 */
transient int size;
已經用於的節點數量

/**
 * 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;
這個是修改的次數

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
極限值,如果節點數大於這個值就需要擴容了,這個值的計算方式是capacity * load factor

/**
 * The load factor for the hash table.
 *
 * @serial
 */
final float loadFactor;
載入因子,決定了節點數值大於當前總數的一定百分比時,擴容

接下來的是建構函式

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
}

如果瞭解當前資料的量的話,建議規定HashMap的大小,減少擴容次數,提高效能

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
這個是從已有的Map中生成一個新的Map,屬於深拷貝

接下來看下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) {
//不為空的話,碰碰運氣,檢查下第一個是不是我們要找到的
//判斷條件是hash值一致,並且key一致
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
//根據下面介紹的雜湊衝突解決方法,瞭解到每個資料項都是一個連結串列,因此需要遍歷這個連結串列去找到我們需要的key那項
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

順便提下,解決雜湊衝突的常見4種方法,1.開放地址,簡單說就是如果當前當前坑被佔了,就繼續找下個坑 2.拉鍊法,也就是JDK中選擇實現HashMap的方法,陣列的每個項又是一個連結串列,如果雜湊後發現當前地址有資料了,就掛在這個連結串列的最後 3.再雜湊 選擇多種雜湊方法,一個不行再換下一個,知道有坑為止 4.建立公共溢位 就把所有溢位的資料都放在溢位表裡

說完get,那就看看put

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
//首先還是先檢查列表是否為空,為空的話就呼叫reszie()新建一個
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
//根據雜湊方法找到列表中的位置,看看是否為空,為空就把這個坑給占上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
//如果坑被佔了,先看看是不是和key一致,一致的話直接把value替換了就行了,都是自己人
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
//再找找連結串列裡有沒有資料項和key一致,不一致的話就找到連結串列的尾部
                if ((e = p.next) == null) {
//在尾部插入一個新的資料項
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
//一致的話先讓p和e指向同一個資料項
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
//檢查e是否為空,不為空的話表示key所對應的資料項已經存在了
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
//這時候就需要判斷下onlyIfAbsent開關,如果為true表示不需要更新已有資料項的值了,false才更新
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
//這個在後面介紹
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

增刪改查,還差一個刪沒講

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
//老套路,先檢查列表是否為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        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 TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
//遍歷列表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
//如果找到對應項
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//改變前後指標,刪除該項
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

再來看看最核心的方法,如何計算得到hash code

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

看懂這個方法首先需要明白^和>>>這個兩個運算子號
^這個符號叫做異或運算子,兩個運算元的位中,相同則結果為0,不同則結果為1
比如2^5=7 因為0010^0101=0111

>>>這個符號叫做無符號右移,忽略符號位,空位都以0補齊
比如5>>>2 0101>>>2等於0001=1,5>>> 0101>>>1 0010=2

再來看下hash這個方法,先取key的hashcode,這個是個native的方法,然後再與右移16位的值取異或。
舉個例子

int h = "hello".hashCode();
System.out.println("result1 : "+ h);
System.out.println("result2 : "+ (h>>>16));
System.out.println("result3 : "+ (h^(h>>>16)));

結果為
result1 : 99162322
result2 : 1513
result3 : 99163451

最後再講下之前提到的modCount的作用。
可以看到modeCount在put,get,remove這些值中被修改。然後在AbstractSet的幾個子類KeySet和Values中的foreach中被用來比較

public final void forEach(Consumer<? super V> action) {
    Node<K,V>[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                action.accept(e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

在迴圈之前先記錄modCount的值,如果迴圈結束之後這個值被改變了說明HashMap內部結構發生了變化,執行緒不安全了,就丟擲異常,從而實現“fast-fail”機制

  • LinkedHashMap

    從名字就可以看出LinkedHashMap繼承於HashMap,它相比於HashMap內部多維護了一個雙向列表,目的就是保證輸入順序和輸出順序一致,帶來的劣勢也很明顯,效能的消耗大大提升。

首先先來看下相比於HashMap增加的幾個成員變數

static class Entry<K,V> extends HashMap.Node<K,V> {
//增加了前面和後面的節點記錄,從而實現雙向連結串列
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

//這個就是雙向連結串列的頭部
transient LinkedHashMap.Entry<K,V> head;
//尾部
transient LinkedHashMap.Entry<K,V> tail;
//雙向連結串列中元素排序規則的標誌位。  
//accessOrder為false,表示按插入順序排序  
//accessOrder為true,表示按訪問順序排序  
final boolean accessOrder;

接著看下增刪改查幾個操作
不知道還記不記得在上面HashMap中講到的put操作中呼叫了一個方法叫newNode
它在LinkedHashMap中被重寫了

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
//如果tail為null代表是個空連結串列,那就把head,tail都指向p
    if (last == null)
        head = p;
    else {
//把之前最後的節點last的after指向新的tail(P)
        p.before = last;
//把P的before指向之前的last,這樣P就成功掛到連結串列的尾部了
        last.after = p;
    }
}

在生成一個新節點的時候,不光把它放到陣列中,還把它放到雙向連結串列的尾部。

看完了增,改再來看看查

public V get(Object key) {
    Node<K,V> e;
//首先呼叫在HashMap中介紹過的getNode方法獲得key對應的節點
    if ((e = getNode(hash(key), key)) == null)
        return null;
//如果按照訪問順序排序的話,那就呼叫afterNodeAccess方法排個序
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

再看看3個HashMap中的空方法,在LinkedHashMap中如何實現

//首先是在上面get中被呼叫的afterNodeAccess
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
//首先確認下該節點是不是已經處於尾部了,如果處於尾部就沒必要動它了
    if (accessOrder && (last = tail) != e) {
//新建3個臨時節點,p就是e,b是e的前面節點,a是e的後面節點
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
//如果e節點前面沒有節點了,那就代表e處於head位置,現在要把它刪掉,所以head指向他後面的a
        if (b == null)
            head = a;
        else
//否則就把它前面的節點b的after指向a
            b.after = a;
//同樣,如果e後面有節點,那就把後面節點a的before指向b,這樣e就成功脫離連結串列了
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
//把p挪到連結串列尾部,這樣就完成了按訪問順序排序了
        tail = p;
        ++modCount;
    }
}

//這個是在remove中呼叫,把e節點從雙向連結串列中刪除
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

//這個是在putVal中被呼叫,根據removeEldestEntry結果決定是否要刪除最老的節點
//這個也LRU的實現核心方法,預設返回的是false,覆蓋這個方法,寫入邏輯就能實現LRU
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

到這基本就明白了LinkedHashMap特點和如何實現
在Android中LRU就是LinkedHashMap的簡單包裝類,有興趣的可以看下

  • TreeMap
    這裡寫圖片描述
    分析TreeMap之前,首先先來了解下NavigableMap和SortedMap這個兩個介面
    SortedMap從名字就可以看出,在Map的基礎上增加了排序的功能。它要求key與key之間是可以相互比較的,從而達到排序的目的。怎麼樣才能比較呢?在之前的Set中提到了comparator.實現了內部排序,這兒,就通過comparator來實現排序。

    而NavigableMap是繼承於SortedMap,目前只有TreeMap和ConcurrentNavigableMap兩種實現方式。它本質上添加了搜尋選項到介面,主要為紅黑樹服務。先來了解下它新增的幾個方法

 /**
     * 返回小於key的最大值的結點
     */
    Map.Entry<K,V> lowerEntry(K key);

    /**
     * 返回小於key的最大值結點的key
     */
    K lowerKey(K key);

    /**
     * 返回小於等於key的最大值結點
     */
    Map.Entry<K,V> floorEntry(K key);

    /**
     * 返回小於等於key的最大結點的key
     */
    K floorKey(K key);

    /**
     * 返回大於等於key的最小結點
     */
    Map.Entry<K,V> ceilingEntry(K key);

    /**
     * 返回大於等於key的最小結點的key
     */
    K ceilingKey(K key);

    /**
     * 返回大於key的最小結點
     */
    Map.Entry<K,V> higherEntry(K key);

    /**
     * 返回大於key的最小結點的key
     */
    K higherKey(K key);

    /**
     * 返回最小key結點
     */
    Map.Entry<K,V> firstEntry();

    /**
     * 返回最大key結點
     */
    Map.Entry<K,V> lastEntry();

    /**
     * 刪除最小key結點
     */
    Map.Entry<K,V> pollFirstEntry();

    /**
     *刪除最大key結點
     */
    Map.Entry<K,V> pollLastEntry();

    /**
     * 獲取相反順序的map
     *
     */
    NavigableMap<K,V> descendingMap();

    /**
     * 返回key的升序迭代器
     */
    NavigableSet<K> navigableKeySet();

    /**
     * 返回key的降序迭代器
     */
    NavigableSet<K> descendingKeySet();

    /**
     * 子map 
     */
    NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
                             K toKey,   boolean toInclusive);

    /**
     * 小於等於toKey的map 
     */
    NavigableMap<K,V> headMap(K toKey, boolean inclusive);

    /**
     * 大於等於key的map
     */
    NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
然後我們來看看TreeMap的實現

首先,TreeMap其實就是一顆紅黑樹
R-B Tree是一種二叉查詢樹,所有符合二叉查詢樹的特點, 對於樹中的每一個節點,如果它有左子樹,則左子樹中所有節點的值不大於該節點值;如果它有右子樹,則右子樹中所有節點的值不小於該節點的值
它本身又有幾個獨特的特性

  1. 每個節點要麼是紅的,要麼是黑的。

  2. 根節點是黑的。

  3. 每個葉節點(葉節點即指樹尾端NIL指標或NULL節點)是黑的。

  4. 如果一個節點是紅的,那麼它的兩個兒子都是黑的。

  5. 對於任一節點而言,其到葉節點樹尾端NIL指標的每一條路徑都包含相同數目的黑節點。

有了這些理論知識,我們再來看看TreeMap的原始碼
先看看節點的資料結構

private static final boolean RED   = false;
private static final boolean BLACK = true;

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;

然後是查

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

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    //如果比較器不為空
    if (comparator != null)
    //就用比較器去查詢
        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;
}

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

再來看看增
之前講定義的時候就提到過,R-B Tree有一些特殊的性質,所以再插入新的節點後,仍需要保持這些性質,需要動態平衡

public V put(K key, V value) {
    Entry<K,V> t = root;
    //如果根節點為空,是一顆空數的話,就把這個節點當做根節點
    //顏色是黑色的
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    //這裡就是二叉查詢樹插入邏輯,小的插左邊,大的插右邊
    Comparator<? super K> cpr = 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
                return t.setValue(value);
        } while (t != null);
    }
    else {
        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++;
    modCount++;
    return null;
}

分析原始碼前,先補充點理論知識
插入有5種不同情況
1) 情況1:插入的是根節點。
原樹是空樹,此情況只會違反性質2。

對策:直接把此節點塗為黑色。
2) 情況2:插入的節點的父節點是黑色。
此不會違反性質2和性質4,紅黑樹沒有被破壞。
對策:什麼也不做。
3) 情況3:當前節點的父節點是紅色且祖父節點的另一個子節點(叔叔節點)是紅色。
此時父節點的父節點一定存在,否則插入前就已不是紅黑樹。與此同時,又分為父節點是祖父節點的左子還是右子,對於對稱性,我們只要解開一個方向就可以了。 在此,我們只考慮父節點為祖父左子的情況。 同時,還可以分為當前節點是其父節點的左子還是右子,但是處理方式是一樣的。我們將此歸為同一類。

對策:將當前節點的父節點和叔叔節點塗黑,祖父節點塗紅,把當前節點指向祖父節點,從新的當前節點重新開始演算法。

情況4:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的右子
對策:當前節點的父節點做為新的當前節點,以新當前節點為支點左旋。

情況5:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的左子
解法:父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋

private void fixAfterInsertion(Entry<K,V> x) {
    //預設插入的是紅色
    x.color = RED;
    //如果插入的父節點也是紅色的
    while (x != null && x != root && x.parent.color == RED) {
        //如果父節點是祖父節點的左節點
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //如果祖父節點的右節點是紅色,符合情況3
            //就把祖父塗紅,父節點和叔父節點塗黑
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //如果叔父節點是黑色,自己是父節點的右子數,符合情況4
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    //對父節點進行左旋轉,下面有圖說明
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            //對稱處理,如果父節點是祖父節點的右節點
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //情況3
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //情況5
                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;
}

左旋轉
這裡寫圖片描述
右旋轉
這裡寫圖片描述

再來看下刪除操作

//首先還是要保證刪除後,還是一個二叉查詢樹
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.
    //如果要刪除的節點有2個子樹,那是最麻煩的情況了
    //需要從左子樹找一個最大的節點或者從右子樹找一個最小的節點來代替要刪除的節點
    if (p.left != null && p.right != null) {
    //這個就是尋找替代節點的方法
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //左子樹存在就取左子樹,否則取右子樹
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    if (replacement != null) {
        // Link replacement to parent
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        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.left = p.right = p.parent = null;

        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        if (p.color == BLACK)
            fixAfterDeletion(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;
        }
    }
}

這裡和增加一樣需要動態調整,同樣有幾種不同的情況
情況1:當前節點是紅+黑色
解法,直接把當前節點染成黑色,結束。此時紅黑樹性質全部恢復。
情況2:當前節點是黑+黑且是根節點
解法:什麼都不做,結束
情況3:當前節點是黑+黑且兄弟節點為紅色(此時父節點和兄弟節點的子節點分為黑)。

解法:把父節點染成紅色,把兄弟節點染成黑色,之後重新進入演算法(我們只討論當前節點是其父節點左孩子時的情況)。此變換後原紅黑樹性質5不變,而把問題轉化為兄弟節點為黑色的情況(注:變化前,原本就未違反性質5,只是為了把問題轉化為兄弟節點為黑色的情況)。
情況4:當前節點是黑加黑且兄弟是黑色且兄弟節點的兩個子節點全為黑色。

解法:把當前節點和兄弟節點中抽取一重黑色追加到父節點上,把父節點當成新的當前節點,重新進入演算法。(此變換後性質5不變)
情況5:當前節點顏色是黑+黑,兄弟節點是黑色,兄弟的左子是紅色,右子是黑色。。

解法:把兄弟節點染紅,兄弟左子節點染黑,之後再在兄弟節點為支點解右旋,之後重新進入演算法。此是把當前的情況轉化為情況6,而性質5得以保持。

情況6:當前節點顏色是黑-黑色,它的兄弟節點是黑色,但是兄弟節點的右子是紅色,兄弟節點左子的顏色任意。
解法:把兄弟節點染成當前節點父節點的顏色,把當前節點父節點染成黑色,兄弟節點右子染成黑色,之後以當前節點的父節點為支點進行左旋,此時演算法結束,紅黑樹所有性質調整正確。

可以對應著看下

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

最後挑一個NavigableMap的導航方法看看,分析下如何實現

//獲取一個大於Key節點的最小節點
// 若不存在(即TreeMap中所有節點的鍵都比key小),就返回null
final Entry<K,V> getCeilingEntry(K key) {
    Entry<K,V> p = root;    
    //從根節點開始遍歷
    while (p != null) {
        int cmp = compare(key, p.key);
        //如果比key大,一直往左找
        if (cmp < 0) {
            if (p.left != null)
                p = p.left;
            else
                return p;
        } else if (cmp > 0) {
            //如果比key小,就往右找
            if (p.right != null) {
                p = p.right;
            } else {
                //一直找到一個節點它的父節點比key大,它自己比key小,並且沒有右子樹
                Entry<K,V> parent = p.parent;
                Entry<K,V> ch = p;
                while (parent != null && ch == parent.right) {
                    ch = parent;
                    parent =