HashMap 詳解六
阿新 • • 發佈:2018-11-28
連結串列轉樹結構
根據詳解四, 當連結串列長度大於 8 時, 為了更高效的查詢, 需要轉成紅黑樹結構, 使用的方法是 treeifyBin. 過程是先把連結串列結構調整為雙向連結串列結構, 再把雙向連結串列結構調整為紅黑樹結構.
/** * tab: 陣列 * hash: 新節點 key 的雜湊值, 通過運算就可以得出需要轉成樹的對應連結串列的對應索引值 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 對於陣列為空或者陣列長度小於閾值, 需要擴容 // 這裡閾值是 64, 陣列長度不是很長, 但是卻需要轉成樹結構, // 說明很多衝突了, 先擴容看看情況有沒有改善, 大概吧 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 { // 建立樹節點, 表示當前節點 TreeNode<K,V> p = replacementTreeNode(e, null); // hd 是頭結點, t1 用來指向下一個節點 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); } } /** * 通過普通連結串列節點構造樹節點 */ TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } /** * 這個方法不是 HashMap 的方法, 是它內部類 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; // 設定根節點 if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; // p 指向樹的當前節點 // x 表示雙向連結串列的當前節點 // 開始遍歷樹 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; // 雙向連結串列的當前節點雜湊值和樹的當前節點雜湊值比較 // 小於就把 dir 設定 -1 // 大於就把 dir 設定 1 // 等於就把 dir 設定 0 // 這裡可以參考上一 part 的 putTreeVal 方法同樣的遍歷方式 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; // 根據 dir 決定向左遍歷還是向右遍歷, 一直到葉子節點 // 然後把雙向連結串列的當前節點插入到葉子節點 // 最後通過 balanceInsertion 方法調整紅黑樹, 參考上一 part 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; } } } } // 調整陣列索引值指向的根節點, 參考上一 part moveRootToFront(tab, root); }
複製紅黑樹
在詳解三講 resize 方法時, 將樹節點複製到新的陣列使用方法 split, 這個方法同樣是 HashMap 內部類 TreeNode 的方法, 下面具體來分析整個複製過程.
/** * map: HashMap 本身 * tab: 新陣列 * index: 舊陣列的索引位置 * bit: 舊陣列長度 */ final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // 跟詳解三一樣, 先設定兩對頭尾節點表示兩個連結串列 // 複製過程是先通過雙向連結串列的遍歷, 然後複製到兩個連結串列, 參考詳解三 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; } } // 陣列索引位置指向連結串列頭結點 // 連結串列長度小於閾值, 把樹節點轉成連結串列節點儲存 // 這裡閾值為 6, 跟連結串列轉樹的閾值 8 不同, 不清楚為什麼要不同 // 連結串列長度大於等於閾值, 把連結串列轉成樹結構, 參考上面的 treeify 方法 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); } } } /** * 把樹節點轉成連結串列節點 */ 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<K,V> p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; }
到這裡, HashMap 插入鍵值對的過程就基本介紹完了, 至於獲取鍵值對就不仔細展開討論了, 涉及到的遍歷都是一樣的. 關於刪除操作以後有機會再來具體分析.