HashMap1.8 源碼解析(3)--TreeNode(紅黑樹) 包括每一個方法

完整代碼:代碼

前言

在寫這篇文章之前,我針對紅黑樹參考算法導(dǎo)論寫了一篇文章圖解紅黑樹-算法導(dǎo)論-java實現(xiàn)基于HashMap1.8,里面的的插入和刪除以及旋轉(zhuǎn)就是用的HashMap1.8里面的代碼,所以里面細(xì)致地分析了balanceDeletion,balanceInsertion,rotateLeft,rotateRight,那這篇主要分析TreeNode中去其他的方法以及一些HashMapTreeNode新加入的屬性和操作.

往紅黑樹中插入元素 putTreeVal方法

在看putTreeVal方法前,先看這幾個函數(shù),因為putTreeVal在函數(shù)體中有調(diào)用這幾個函數(shù)

comparableClassFor方法
 /**
     * 
     * @param x 對象x
     * @return 如果x所屬的類實現(xiàn)了Comparable<T>接口,并且T是x類 比如 class Person implements Comparable<Person>
     *         如果class Person 或者類似于 class Person implements Comparable 或者 class Person implements Comparable<String>都會返回null
     */
    static Class<?> comparableClassFor(Object x) {
        if (x instanceof Comparable) {
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // 如果是String類直接返回了
                return c;
            /**
             * getGenericInterfaces():
             *       Returns the Types representing the interfaces directly implemented
             *       by the class or interface represented by this object.
             * 就是返回c類實現(xiàn)的所有接口
             */
            if ((ts = c.getGenericInterfaces()) != null) {   
                for (int i = 0; i < ts.length; ++i) {
                        /**
                         *  Comparable<Person> 如果此接口含有泛型 并且原型class是Compable類
                         *  getActualTypeArguments() 返回這個類里面所有的泛型 返回一個數(shù)組
                         *  as.length == 1 && as[0] == c 是為了保證Comparable<T>里面只有一個泛型并且就是傳入的類c
                         */
                    
                    if (((t = ts[i]) instanceof ParameterizedType) &&
                        ((p = (ParameterizedType)t).getRawType() ==
                         Comparable.class) &&
                        (as = p.getActualTypeArguments()) != null &&
                        as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }

給一個最直觀的測試讓大家明白這個函數(shù)的作用.

Person類的定義 調(diào)用 返回
class Person comparableClassFor(new Person("Tom", 12)) null
class Person implements Comparable comparableClassFor(new Person("Tom", 12)) null
class Person implements Comparable<String> comparableClassFor(new Person("Tom", 12)) null
class Person implements Comparable<Person> comparableClassFor(new Person("Tom", 12)) Person
compareComparables方法

方法很簡單,就是在滿足條件的情況下調(diào)用CompareTo方法返回,否則返回0

/**
     * Returns k.compareTo(x) if x matches kc (k's screened comparable
     * class), else 0.
     * 如果類不同就返回0 否則就返回調(diào)用compareTo比較后的結(jié)果
     */
    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
    static int compareComparables(Class<?> kc, Object k, Object x) {
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }
tieBreakOrder方法

identityHashCode()和hashCode()的區(qū)別會在另外一篇博客待完成中分析

      /**
         * a或者b為null或者如果a和b同屬一個類就調(diào)用系統(tǒng)的identityHashCode
         */
        static int tieBreakOrder(Object a, Object b) {
            int d;
            if (a == null || b == null ||
                (d = a.getClass().getName().
                 compareTo(b.getClass().getName())) == 0)
                d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                     -1 : 1);
            return d;
        }
root方法

因為當(dāng)前的節(jié)點可能不是紅黑樹的根,為什么呢?

1.紅黑樹中的每個節(jié)點都是TreeNode節(jié)點,所以每個節(jié)點都可以調(diào)用TreeNode中的方法.

final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
find方法

TreeNode<K,V> find(int h, Object k, Class<?> kc)方法就是二叉樹的查找了,查找該k和對應(yīng)的h是否存在在以當(dāng)前節(jié)點為頭結(jié)點的子樹中了.

代碼就不貼了,比較簡單.

putTreeVal方法

因為紅黑樹插入的時候需要比較TreeNode,這里是把節(jié)點的hash值來比較大小,具體比較機制在代碼的注釋中有解釋.

至于為什么不直接用CompareTo方法來比較,因為在HashMap 1.7的時候沒有引入紅黑樹,所以大部分的代碼的Key是可能沒有實現(xiàn)Comparable接口,因此我猜應(yīng)該是為了兼容的問題.

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 循環(huán)去尋找到該節(jié)點應(yīng)該插入的位置,TreeNode之間的比較是通過該節(jié)點的hash值
             *     1. 如果該節(jié)點已經(jīng)存在 那就直接返回
             *     2. 如果該節(jié)點hash值小于該節(jié)點的hash值 往左側(cè)
             *     3. 如果該節(jié)點hash值小于該節(jié)點的hash值 往右側(cè)
             *     
             */
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if ((ph = p.hash) > h) // 左側(cè)
                    dir = -1;
                else if (ph < h)      // 右側(cè)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk))) //已經(jīng)存在
                    return p;
                /**
                 * hash值相等但是key值沒有匹配上會進行以下操作
                 * 1. 調(diào)用comparableClassFor先看該k是否已經(jīng)實現(xiàn)compareable<k.class>接口
                 * 2. 如果實現(xiàn)了Compareable<k.class>接口 就進行compareComparables方法看看是否可以比較出結(jié)果
                 * 3. 如果還是沒有比較出結(jié)果,就去左右子樹中搜索有沒有該節(jié)點(注意只搜索一次)
                 * 4. 如果左右子樹中也沒有該節(jié)點,說明必須要插入該節(jié)點到此紅黑樹中,所以就調(diào)用tieBreakOrder強行分出大小
                 */
                else if ((kc == null &&                                    
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    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;
                    }
                    dir = tieBreakOrder(k, pk);
                }
                
                /**
                 * 觀察當(dāng)前節(jié)點是進入左側(cè) 還是進入右側(cè) 還是插入
                 * 如果是插入的時候會進入if block 中的內(nèi)容
                 */
                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);  //生成節(jié)點
                    //插入到紅黑樹中
                    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;
                    /**
                     * 調(diào)整紅黑樹返回新的節(jié)點值
                     * 把返回新的節(jié)點值移到鏈表的頭
                     */
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

balanceInsertion方法在圖解紅黑樹-算法導(dǎo)論-java實現(xiàn)基于HashMap1.8已經(jīng)分析過了

由于HashMap在樹化的過程中既保持了紅黑樹的結(jié)構(gòu),并且也保持了原先鏈表中的結(jié)構(gòu),只不過這個鏈表與其他沒有樹化的鏈表的區(qū)別在于樹化的鏈表的節(jié)點類型是TreeNode,而沒有樹化的鏈表的節(jié)點類型是Node,所以當(dāng)新節(jié)點在插入的時候既要往紅黑樹中插入,也要往鏈表中插入.

所以在balanceInsertion的過程中有可能會通過旋轉(zhuǎn)root會發(fā)生改變,因此moveRootToFront的作用就是把root節(jié)點move到鏈表的頭.

 static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            if (root != null && tab != null && (n = tab.length) > 0) {
                    //計算在哪個bin 
                int index = (n - 1) & root.hash;
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                //如果鏈表的頭不是紅黑樹的頭節(jié)點 則把root移到頭節(jié)點 也就是first的前面
                if (root != first) {
                    Node<K,V> rn;
                    tab[index] = root;
                    TreeNode<K,V> rp = root.prev;
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    if (rp != null)
                        rp.next = rn;
                    if (first != null)
                        first.prev = root;
                    root.next = first;
                    root.prev = null;
                }
                // 檢查一下紅黑樹的各個成員變量是否正常
                assert checkInvariants(root);
            }
        }
checkInvariants方法

循環(huán)檢查紅黑樹每個節(jié)點的成員變量是否正常.

static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
            TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
                tb = t.prev, tn = (TreeNode<K,V>)t.next;
            if (tb != null && tb.next != t)
                return false;
            if (tn != null && tn.prev != t)
                return false;
            if (tp != null && t != tp.left && t != tp.right)
                return false;
            if (tl != null && (tl.parent != t || tl.hash > t.hash))
                return false;
            if (tr != null && (tr.parent != t || tr.hash < t.hash))
                return false;
            if (t.red && tl != null && tl.red && tr != null && tr.red)
                return false;
            if (tl != null && !checkInvariants(tl))
                return false;
            if (tr != null && !checkInvariants(tr))
                return false;
            return true;
        }

樹化 treeify方法

調(diào)用該方法的TreeNode節(jié)點是一個從原先的Node類型的鏈表轉(zhuǎn)化成TreeNode中的頭節(jié)點,看原因在哪里? 在treeifyBin方法中可以找到答案的.

 /**
         * @return 調(diào)用該方法的時候this就已經(jīng)是一個TreeNode了 
         *         而且整個鏈表的節(jié)點類型已經(jīng)從Node轉(zhuǎn)換成TreeNode 但是順序沒有變化
         */
        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;
                // 插入的是第一個元素 并給root賦值
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //插入到紅黑樹中的位置 邏輯跟putTreeVal類似
                    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;
                        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;
                        }
                    }
                }
            }
            // 把root節(jié)點移到鏈表頭
            moveRootToFront(tab, root);
        }

鏈表化untreeify

當(dāng)紅黑樹中的節(jié)點個數(shù)等于6,就把紅黑樹轉(zhuǎn)化成簡單的鏈表類型

final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            for (Node<K,V> q = this; q != null; q = q.next) {
                    //將Node轉(zhuǎn)化成TreeNode
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

擴容時轉(zhuǎn)移節(jié)點 split

思想與鏈表的轉(zhuǎn)移節(jié)點類似,根據(jù)rehash的特殊性,把紅黑樹拆成兩個鏈表,然后再根據(jù)兩個鏈表的長度決定是否樹化或者鏈表化.對原理不明白的可以參考另一篇文章HashMap1.8 源碼解析(1)--插入元素rehash部分.

有人可能對鏈表化有疑問?畢竟已經(jīng)是鏈表了啊,為什么還需要進行鏈表化,答案是因為此時的鏈表是TreeNode類型的,需要轉(zhuǎn)化成Node類型的.

        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // 將紅黑樹根據(jù)rehash分成兩個鏈表
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            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;
                }
            }
            
            //根據(jù)每個鏈表的長度來決定是樹化還是鏈表化

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

小例子

public class Person {
    String name;
    int age;
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age  = age;
    }
    
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
    @Override
    public boolean equals(Object obj) {

        if (this == obj) return true;
        if (obj instanceof Person) {
            Person p = (Person)obj;
            return p.name.equals(this.name);
        }
        return false;

        //return true;
    }
    
    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        return age;
    }
}
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class TestHashMap {

    public static void main(String[] args) {
        test_redblacktree_put();
        //test_system_hashcode();
    }
    
    public static void test_redblacktree_put() {
        HashMap<Person, Integer> map = new HashMap<>(64);
        char[] strs = "SEARCHXMPL".toCharArray();
        for (int i = 0; i < strs.length; i++) {
            //System.out.println("insert into " + strs[i]);
            map.put(new Person(strs[i] + "", (strs[i] - '0') * 64),  i);
            printMap(map);
        }
        map.put(new Person("Z", ('M' - '0') * 64), -1);  //will goto tieBreak block
        printMap(map);
    }

    private static void printMap(HashMap<Person, Integer> map) {
        HashMap.Node<Person, Integer>[] table = map.table; 
        for (int i = 0; i < table.length; i++) {
            HashMap.Node<Person, Integer> e;
            if ((e = table[i]) != null) {
                System.out.print(i + ":");
                System.out.print(e);
                HashMap.Node<Person, Integer> p;
                while ((p = e.next) != null) {
                    System.out.print("->" + p);
                    e = e.next;
                }
                System.out.println();
            }
        }
    }
}

簡單看一下結(jié)果:

插入到P時才進行樹化

image.png

樹化結(jié)束時的紅黑樹結(jié)構(gòu)以及鏈表結(jié)構(gòu) 此時E是鏈表和紅黑樹的頭

當(dāng)插入節(jié)點L時,紅黑樹的root節(jié)點發(fā)生了變化成為了M,通過moveRootToFront方法把M移到E的前面成為鏈表的頭結(jié)點.

image.png

建議感興趣的人可以下載完整代碼自己調(diào)試一下看看結(jié)果如何,對理解紅黑樹會有更清晰些.

至此HashMapTreeNode所有的方法都已經(jīng)分析了,希望可以對你有所幫助,如果哪里寫得有問題,還請指正哈.

1.HashMap1.8 源碼解析(1)--插入元素
2.HashMap1.8 源碼解析(2)--刪除元素
3.HashMap1.8 源碼解析(3)--TreeNode(紅黑樹)包括每一個方法
4.HashMap1.8 源碼解析(4)--遍歷元素

參考

1.java1.8 源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容