HashMap什么時候會觸發鏈表轉紅黑樹


日常工作中,被同事突然問到的一個問題,hashmap是我們JAVA程序中使用頻率非常高的key-value鍵值對形式的數據類型

結論是目前能觸發轉化的兩個條件是:一個是鏈表的長度達到8個,一個是數組的長度達到64個

為什么要觸發這個轉換,目前官方的解釋:

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-pow(0.5, k) / factorial(k)). The first values are:

0: 0.60653066

1: 0.30326533

2: 0.07581633

3: 0.01263606

4: 0.00157952

5: 0.00015795

6: 0.00001316

7: 0.00000094

8: 0.00000006

more: less than 1 in ten million

翻譯過來大概的意思是:理想情況下使用隨機的哈希碼,容器中節點分布在hash桶中的頻率遵循泊松分布(具體可以查看http://en.wikipedia.org/wiki/Poisson_distribution),按照泊松分布的計算公式計算出了桶中元素個數和概率的對照表,可以看到鏈表中元素個數為8時的概率已經非常小,再多的就更少了,所以原作者在選擇鏈表元素個數時選擇了8,是根據概率統計而選擇的

 

JDK1.8中對hashmap底層對實現進行了優化,引入了紅黑樹對數據結構和擴容優化等,先看下源碼:

/**
 * Implements Map.put and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    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;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果當前的bucket里面已經是紅黑樹的話,執行紅黑樹的添加操作 
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
            // TREEIFY_THRESHOLD = 8,判斷如果當前bucket的位置鏈表長度大於8的話就將此鏈表變成紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 



這個方法執行對操作是,先通過hash計算要添加的key准備插入的槽位,如果key是一樣的,則根據設置的參數是否執行覆蓋,如果相應的槽位是空的話直接插入,如果對應的槽位有值則判斷是紅黑樹結構還是鏈表結構,
鏈表的話則順着鏈表尋找,如果找到一樣的key,則根據參數選擇覆蓋,沒有找到則鏈接到鏈表最后面,鏈表項的數目大於8則對其進行樹化,如果是紅黑樹結構則按照樹的添加方式進行添加操作
進行數化對添加操作是通過treeifyBin方法,我們來看看這個方法:

/**
 * Replaces all linked nodes in bin at index for given hash unless
 * table is too small, in which case resizes instead.
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //MIN_TREEIFY_CAPACITY = 64,這里是重點,如果table小於64,那么是走的擴容resize的方法,超過這個數字,才會走到else的TreeNode的構建 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    //自動擴容,后續專門一篇文章介紹這個
        resize();
    // 通過hash求出bucket的位置。
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
      // 將每個節點包裝成TreeNode
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
      // 將所有TreeNode連接在一起此時只是鏈表結構
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
這個方法的操作是將目前的9個項通過鏈表的方式鏈接在一起,以他為基礎,構建紅黑樹
看下TreeNode類的源碼,發現對紅黑樹的操作都是在TreeNode內部
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,

/**
 * Forms tree of the nodes linked from this node.
 */
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 {
//x是當前訪問鏈表中的項
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
//此時紅黑樹已經有了根節點,上面獲取了當前加入紅黑樹的項的key和hash值進入核心循環,從root開始是一個自頂向下的方式遍歷添加
//for循環沒有控制條件,由代碼內部break跳出循環
            for (TreeNode<K,V> p = root;;) {
//dir:directory,ph:parent hash
                int dir, ph;
                K pk = p.key;
//比較當前項與當前樹中訪問節點hash值判斷加入項的路徑,-1為左子樹,+1為右子樹

                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);
//xp:x parent,找到符合x添加條件的節點
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
//如果xp的hash值大於x的hash值,將x添加在xp的左邊,否則,添加在xp的右邊
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
//添加節點后,維護添加后紅黑樹的紅黑結構
                    root = balanceInsertion(root, x);
//跳出循環,代表當前鏈表中的項成功的添加到了紅黑樹中
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

 


整個方法的大體的執行是第一次循環會將鏈表中的第一個節點作為紅黑樹的根,后面的循環會通過比較鏈表中的項的hash值,放到樹節點的左邊或者右邊,因為添加操作可能會破壞樹的結構,所以最后會做一次
balanceInsertion
這個方法里面將會進行旋轉和顏色變換,具體的原理就是依據紅黑樹的規則。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM