HashMap源碼分析html
HashMap的底層實現是面試中問到最多的,其原理也更加複雜,涉及的知識也越多,在項目中的使用也最多。所以清晰分析出其底層源碼對於深入理解其實現有重要的意義,jdk1.8以後其設計與實現也有所改變。node
在Java集合類中最經常使用的除了ArrayList外,就是HashMap了。Java最基本的數據結構有數組和鏈表。數組的特色是空間連續(大小固定)、尋址迅速,可是插入和刪除時須要移動元素,因此查詢快,增長刪除慢。鏈表剛好相反,可動態增長或減小空間以適應新增和刪除元素,但查找時只能順着一個個節點查找,因此增長刪除快,查找慢。有沒有一種結構綜合了數組和鏈表的優勢呢?固然有,那就是哈希表(雖然說是綜合優勢,但實際上查找確定沒有數組快,插入刪除沒有鏈表快,一種折中的方式吧)。面試
因其底層哈希桶的數據結構是數組,因此也會涉及到擴容的問題。當HashMap的容量達到threshold域值時,就會觸發擴容。擴容先後,哈希桶的長度必定會是2的次方。這樣在根據key的hash值尋找對應的哈希桶時,能夠用位運算替代取餘操做,更加高效。數組
擾動函數就是爲了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,所以在與運算時,至關於高低位一塊兒參與了運算,以減小hash碰撞的機率。(在JDK8以前,擾動函數會擾動四次,JDK8簡化了這個操做)安全
擴容時,若是發生過哈希碰撞,節點數小於8個。則要根據鏈表上每一個節點的哈希值,依次放入新哈希桶對應下標位置。數據結構
由於擴容是容量翻倍,因此原鏈表上的每一個節點,如今可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量。
1、類聲明app
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap繼承自AbstractMap,實現了Map接口,Map接口定義了全部Map子類必須實現的方法。AbstractMap也實現了Map接口,而且提供了兩個實現Entry的內部類:SimpleEntry和SimpleImmutableEntry。函數
2、成員變量源碼分析
//默認的初始容量,必須是2的冪。 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; //默認裝載因子,默認值爲0.75,若是實際元素所佔容量佔分配容量的75%時就要擴容了。若是填充比很大,說明利用的空間不少,可是查找的效率很低,由於鏈表的長度很大(固然最新版本使用了紅黑樹後會改進不少),HashMap原本是以空間換時間,因此填充比不必太大。可是填充比過小又會致使空間浪費。若是關注內存,填充比能夠稍大,若是主要關注查找性能,填充比能夠稍小。 static final float _LOAD_FACTOR = 0.75f; //一個桶的樹化閾值 //當桶中元素個數超過這個值時,須要使用紅黑樹節點替換鏈表節點 //這個值必須爲 8,要否則頻繁轉換效率也不高 static final int TREEIFY_THRESHOLD = 8; //一個樹的鏈表還原閾值 //當擴容時,桶中元素個數小於這個值,就會把樹形的桶元素 還原(切分)爲鏈表結構 //這個值應該比上面那個小,至少爲 6,避免頻繁轉換 static final int UNTREEIFY_THRESHOLD = 6; //哈希表的最小樹形化容量 //當哈希表中的容量大於這個值時,表中的桶才能進行樹形化 //不然桶內元素太多時會擴容,而不是樹形化 //爲了不進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64; //存儲數據的Entry數組,長度是2的冪。 transient Entry[] table; // transient Set<Map.Entry<K,V>> entrySet; //map中保存的鍵值對的數量 transient int size; //須要調整大小的極限值(容量*裝載因子) int threshold; //裝載因子 final float loadFactor; //map結構被改變的次數 transient volatile int modCount;
內部類,鏈表節點Node:性能
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue())) return true; } return false; } }
3、構造方法
/** *使用默認的容量及裝載因子構造一個空的HashMap */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } /** * 根據給定的初始容量和裝載因子建立一個空的HashMap * 初始容量小於0或裝載因子小於等於0將報異常 */ 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); } /** *根據指定容量建立一個空的HashMap */ public HashMap(int initialCapacity) { //調用上面的構造方法,容量爲指定的容量,裝載因子是默認值 this(initialCapacity, DEFAULT_LOAD_FACTOR); } //經過傳入的map建立一個HashMap,容量爲默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子爲默認值 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
HashMap提供了四種構造方法:
(1)使用默認的容量及裝載因子構造一個空的HashMap;
(2)根據給定的初始容量和裝載因子建立一個空的HashMap;
(3)根據指定容量建立一個空的HashMap;
(4)經過傳入的map建立一個HashMap。
第三種構造方法會調用第二種構造方法,而第四種構造方法將會調用putMapEntries方法將元素添加到HashMap中去。
putMapEntries方法是一個final方法,不能夠被修改,該方法實現了將另外一個Map的全部元素加入表中,參數evict初始化時爲false,其餘狀況爲true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { //根據m的元素數量和當前表的加載因子,計算出閾值 float ft = ((float)s / loadFactor) + 1.0F; //修正閾值的邊界 不能超過MAXIMUM_CAPACITY int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY); //若是新的閾值大於當前閾值 if (t > threshold) //返回一個>=新的閾值的 知足2的n次方的閾值 threshold = tableSizeFor(t); } //若是當前元素表不是空的,可是 m的元素數量大於閾值,說明必定要擴容。 else if (s > threshold) resize(); //遍歷 m 依次將元素加入當前表中。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
其中,涉及到兩個操做,一個是計算新的閾值,另外一個是擴容方法:
1)若是新的閾值大於當前閾值,須要返回一個>=新的閾值的 知足2的n次方的閾值,這涉及到了tableSizeFor:
static final int tableSizeFor(int cap) { //通過下面的 或 和位移 運算, n最終各位都是1。 int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; //判斷n是否越界,返回 2的n次方做爲 table(哈希桶)的閾值 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
2)若是當前元素表不是空的,可是 m的元素數量大於閾值,說明必定要擴容。這涉及到了擴容方法resize,這是我的認爲HashMap中最複雜的方法:
final Node<K,V>[] resize() { //oldTab 爲當前表的哈希桶 Node<K,V>[] oldTab = table; //當前哈希桶的容量 length int oldCap = (oldTab == null) ? 0 : oldTab.length; //當前的閾值 int oldThr = threshold; //初始化新的容量和閾值爲0 int newCap, newThr = 0; //若是當前容量大於0 if (oldCap > 0) { //若是當前容量已經到達上限 if (oldCap >= MAXIMUM_CAPACITY) { //則設置閾值是2的31次方-1 threshold = Integer.MAX_VALUE; //同時返回當前的哈希桶,再也不擴容 return oldTab; }//不然新的容量爲舊的容量的兩倍。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //若是舊的容量大於等於默認初始容量16 //那麼新的閾值也等於舊的閾值的兩倍 newThr = oldThr << 1; // double threshold } //若是當前表是空的,可是有閾值。表明是初始化時指定了容量、閾值的狀況 else if (oldThr > 0) newCap = oldThr;//那麼新表的容量就等於舊的閾值 else { //若是當前表是空的,並且也沒有閾值。表明是初始化時沒有任何容量/閾值參數的狀況 newCap = DEFAULT_INITIAL_CAPACITY;//此時新表的容量爲默認的容量 16 //新的閾值爲默認容量16 * 默認加載因子0.75f = 12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { //若是新的閾值是0,對應的是 當前表是空的,可是有閾值的狀況 float ft = (float)newCap * loadFactor;//根據新表容量 和 加載因子 求出新的閾值 //進行越界修復 newThr = (newCap < MAXIMUM_CAPACITY && ft <(float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //更新閾值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //根據新的容量 構建新的哈希桶 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //更新哈希桶引用 table = newTab; //若是之前的哈希桶中有元素 //下面開始將當前哈希桶中的全部節點轉移到新的哈希桶中 if (oldTab != null) { //遍歷老的哈希桶 for (int j = 0; j < oldCap; ++j) { //取出當前的節點 e Node<K,V> e; //若是當前桶中有元素,則將鏈表賦值給e if ((e = oldTab[j]) != null) { //將原哈希桶置空以便GC oldTab[j] = null; //若是當前鏈表中就一個元素,(沒有發生哈希碰撞) if (e.next == null) //直接將這個元素放置在新的哈希桶裏。 //注意這裏取下標 是用 哈希值 與 桶的長度-1 。 因爲桶的長度是2的n次方,這麼作實際上是等於 一個模運算。可是效率更高 newTab[e.hash & (newCap - 1)] = e; //若是發生過哈希碰撞 ,並且是節點數超過8個,轉化成了紅黑樹 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是發生過哈希碰撞,節點數小於8個。則要根據鏈表上每一個節點的哈希值,依次放入新哈希桶對應下標位置。 else { //由於擴容是容量翻倍,因此原鏈表上的每一個節點,如今可能存放在原來的下標,即low位,或者擴容後的下標,即high位。high位=low位+原哈希桶容量 //低位鏈表的頭結點、尾節點 Node<K,V> loHead = null, loTail = null; //高位鏈表的頭節點、尾節點 Node<K,V> hiHead = null, hiTail = null; Node<K,V> next;//臨時節點 存放e的下一個節點 do { next = e.next; //利用位運算代替常規運算:利用哈希值與舊的容量,能夠獲得哈希值去模後,是大於等於oldCap仍是小於oldCap,等於0表明小於oldCap,應該存放在低位,不然存放在高位 if ((e.hash & oldCap) == 0) { //給頭尾節點指針賦值 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; }//高位也是相同的邏輯 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; }//循環直到鏈表結束 } while ((e = next) != null); //將低位鏈表存放在原index處 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //將高位鏈表存放在新index處 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
resize的操做主要涉及如下幾步操做:
4、成員方法
//向哈希表中添加元素 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
向用戶開放的put方法調用的是putVal方法:
putVal方法須要判斷是否出現哈希衝突問題:
其中若是哈希值相等,key也相等,則是覆蓋value操做;若是不是覆蓋操做,則插入一個普通鏈表節點;
遍歷到尾部,追加新節點到尾部;
在元素添加的過程當中須要隨時檢查是否須要進行轉換成紅黑樹的操做;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab存放當前的哈希桶,p用做臨時鏈表節點 Node<K,V>[] tab; Node<K,V> p; int n, i; //若是當前哈希表是空的,表明是初始化 if ((tab = table) == null || (n = tab.length) == 0) //那麼直接去擴容哈希表,而且將擴容後的哈希桶長度賦值給n n = (tab = resize()).length; //若是當前index的節點是空的,表示沒有發生哈希碰撞。直接構建一個新節點Node,掛載在index處便可。 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;//將當前節點引用賦值給e else if (p instance of 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); //若是追加節點後,鏈表數量>=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; } } //若是e不是null,說明有須要覆蓋的節點, if (e != null) { // existing mapping for key //則覆蓋節點值,並返回原oldValue V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //這是一個空實現的函數,用做LinkedHashMap重寫使用。 afterNodeAccess(e); return oldValue; } } //若是執行到了這裏,說明插入了一個新的節點,因此會修改modCount,以及返回null。 ++modCount; //更新size,並判斷是否須要擴容。 if (++size > threshold) resize(); //這是一個空實現的函數,用做LinkedHashMap重寫使用。 afterNodeInsertion(evict); return null; }
當存入的key是null的時候將調用putVal方法,看key不爲null的狀況。先調用了hash(int h)方法獲取了一個hash值。
「擾動函數」,這個方法的主要做用是防止質量較差的哈希函數帶來過多的衝突(碰撞)問題。Java中int值佔4個字節,即32位。根據這32位值進行移位、異或運算獲得一個值。
那HashMap中最核心的部分就是哈希函數,又稱散列函數。也就是說,哈希函數是經過把key的hash值映射到數組中的一個位置來進行訪問。
hashCode右移16位,正好是32bit的一半。與本身自己作異或操做(相同爲0,不一樣爲1)。就是爲了混合哈希值的高位和低位,增長低位的隨機性。而且混合後的值也變相保持了高位的特徵。
HashMap之因此不能保持元素的順序有如下幾點緣由:
//只作一次16位右位移異或混合: static int hash(Object key) { int h; return (key == null) ? 0 : (h=key.hashCode())^(h>>>16); }
其中,key.hashCode()是Key自帶的hashCode()方法,返回一個int類型的散列值。咱們你們知道,32位帶符號的int表值範圍從-2147483648到2147483648。這樣只要hash函數鬆散的話,通常是很難發生碰撞的,由於HashMap的初始容量只有16。可是這樣的散列值咱們是不能直接拿來用的。用以前須要對數組的長度取模運算。獲得餘數纔是索引值。
indexFor返回hash值和table數組長度減1的與運算結果。爲何使用的是length-1?由於這樣能夠保證結果的最大值是length-1,不會產生數組越界問題。
static int indexFor(int h, int length) { return h & (length-1); }
public V get(Object key) { Node<K,V> e; //傳入擾動後的哈希值 和 key 找到目標節點Node return (e = getNode(hash(key), key)) == null ? null : e.value; }
HashMap向用戶分開放的get方法是調用的getNode方法來實現的,
//傳入擾動後的哈希值 和 key 找到目標節點Node final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //查找過程,找到返回節點,不然返回null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && ((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 { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
查找的判斷條件是:e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))),在比較hash值的同時須要比較key的值是否相同。
1)contains
HashMap沒有提供判斷元素是否存在的方法,只提供了判斷Key是否存在及Value是否存在的方法,分別是containsKey(Object key)、containsValue(Object value)。
containsKey(Object key)方法很簡單,只是判斷getNode (key)的結果是否爲null,是則返回false,否返回true。
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } public boolean containsValue(Object value) { Node<K,V>[] tab; V v; //遍歷哈希桶上的每個鏈表 if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { //若是找到value一致的返回true if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }
判斷一個value是否存在比判斷key是否存在還要簡單,就是遍歷全部元素判斷是否有相等的值。這裏分爲兩種狀況處理,value爲null何不爲null的狀況,但內容差很少,只是判斷相等的方式不一樣。這個判斷是否存在必須遍歷全部元素,是一個雙重循環的過程,所以是比較耗時的操做。
2)remove方法
HashMap中「刪除」相關的操做,有remove(Object key)和clear()兩個方法。
其中向用戶開放的remove方法調用的是removeNode方法,,removeNode (key)的返回結果應該是被移除的元素,若是不存在這個元素則返回爲null。remove方法根據removeEntryKey返回的結果e是否爲null返回null或e.value。
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) { // p 是待刪除節點的前置節點 Node<K,V>[] tab; Node<K,V> p; int n, index; //若是哈希表不爲空,則根據hash值算出的index下 有節點的話。 if ((tab = table) != null && (n = tab.length) > 0&&(p = tab[index = (n - 1) & hash]) != null) { //node是待刪除節點 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;//將待刪除節點引用賦給node else if ((e = p.next) != null) {//不然循環遍歷 找到待刪除節點,賦值給node 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); } } //若是有待刪除節點node, 且 matchValue爲false,或者值也相等 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)//若是node == p,說明是鏈表頭是待刪除節點 tab[index] = node.next; else//不然待刪除節點在表中間 p.next = node.next; ++modCount;//修改modCount --size;//修改size afterNodeRemoval(node);//LinkedHashMap回調函數 return node; } } return null; }
clear()方法刪除HashMap中全部的元素,這裏就不用一個個刪除節點了,而是直接將table數組內容都置空,這樣全部的鏈表都已經沒法訪問,Java的垃圾回收機制會去處理這些鏈表。table數組置空後修改size爲0。
public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
5、樹形化和紅黑樹的操做
能夠看到不管是put,get仍是remove方法中都有if (node instanceof TreeNode)方法來判斷當前節點是不是一個樹形化的節點,若是是的話就須要調用相應的紅黑樹的相關操做。
TreeNode<K,V> parent; // 父節點 TreeNode<K,V> left; //左節點 TreeNode<K,V> right; //右節點 TreeNode<K,V> prev; // 在鏈表中的前一個節點 boolean red; //染紅或者染黑標記
桶的樹形化 treeifyBin(),若是一個桶中的元素個數超過 TREEIFY_THRESHOLD(默認是 8 ),就使用紅黑樹來替換鏈表,從而提升速度。這個替換的方法叫 treeifyBin() 即樹形化。
//將桶內全部的 鏈表節點 替換成 紅黑樹節點 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(); //若是哈希表中的元素個數超過了樹形化閾值,進行樹形化,e是哈希表中指定位置桶裏的鏈表節點,從第一個開始 else if ((e = tab[index = (n - 1) & hash]) != null) { //新建一個樹形節點,內容和當前鏈表節點e一致 TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); 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); } }
這段代碼很簡單,只是對桶裏面的每一個元素調用了replacementTreeNode方法將當前的節點變爲一個樹形節點來進行樹形化:
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
在全部的節點都替換成樹形節點後須要讓桶的第一個元素指向新建的紅黑樹頭結點,之後這個桶裏的元素就是紅黑樹而不是鏈表了,以前的操做並無設置紅黑樹的顏色值,如今獲得的只能算是個二叉樹。在最後調用樹形節點 hd.treeify(tab) 方法進行塑造紅黑樹,這是HashMap中我的認爲第二個比較難的方法:
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; //又一個循環,從根節點開始,遍歷全部節點跟當前節點 x 比較,調整位置,有點像冒泡排序 for (TreeNode<K,V> p = root;;) { int dir, ph; //這個 dir K pk = p.key; if ((ph = p.hash) > h) //當比較節點的哈希值比 x 大時,dir 爲 -1 dir = -1; else if (ph < h) //哈希值比 x 小時 dir 爲 1 dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) // 若是比較節點的哈希值x dir = tieBreakOrder(k, pk); //把當前節點變成 x 的父親 //若是當前比較節點的哈希值比 x 大,x 就是左孩子,不然 x 是右孩子 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; } } } } moveRootToFront(tab, root); }
能夠看到,將二叉樹變爲紅黑樹時,須要保證有序。這裏有個雙重循環,拿樹中的全部節點和當前節點的哈希值進行對比(若是哈希值相等,就對比鍵,這裏不用徹底有序),而後根據比較結果肯定在樹種的位置。
紅黑樹的基本要求:
紅黑樹是一種近似平衡的二叉查找樹,它可以確保任何一個節點的左右子樹的高度差不會超過兩者中較低那個的一倍。它不是嚴格控制左、右子樹高度或節點數之差小於等於1,但紅黑樹高度依然是平均log(n),且最壞狀況高度不會超過2log(n)。紅黑樹是知足以下條件的二叉查找樹(binary search tree):
null
(樹尾端)的任何路徑,都含有相同個數的黑色節點。上面的方法treeify涉及到的修正紅黑樹的方法balanceInsertion方法須要對樹中節點進行從新的染色,這個函數也是紅黑樹樹插入數據時須要調用的函數,其中涉及到的是左旋和右旋操做,這也是紅黑樹中兩個主要的操做:
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { //插入的節點必須是紅色的,除非是根節點 x.red = true; //遍歷到x節點爲黑色,整個過程是一個上濾的過程 xp=x.parent;xpp=xp.parent;xppl=xpp.left;xppr=xpp.right; for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { if ((xp = x.parent) == null) { x.red = false; return x; } //若是xp是黑色就直接完成,最簡單的狀況 else if (!xp.red || (xpp = xp.parent) == null) return root; //若是x的父節點是xp父節點的左節點 if (xp == (xppl = xpp.left)) { //x的父親節點的兄弟是紅色的(須要顏色翻轉)case1 if ((xppr = xpp.right) != null && xppr.red) { xppr.red = false; //x父親節點的兄弟節點置成黑色 xp.red = false; //父親和其兄弟節點同樣是黑色 xpp.red = true; //祖父節點置成紅色 x = xpp; //而後上濾(就是不斷的重複上面的操做) } else { //若是x是xp的右節點整個要進行兩次旋轉,先左旋轉再右旋轉 // case2 if (x == xp.right) { root = rotateLeft(root, x = xp);//左旋 xpp = (xp = x.parent) == null ? null : xp.parent; } //case3 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp);//右旋 } } } } //以左節點鏡像對稱 else { if (xppl != null && xppl.red) { xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } else { if (x == xp.left) { root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateLeft(root, xpp); } } } } } }
左旋操做和右旋操做,拿出其中的代碼:
//左旋轉 static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> r, pp, rl; if (p != null && (r = p.right) != null) { if ((rl = p.right = r.left) != null) rl.parent = p; if ((pp = r.parent = p.parent) == null) (root = r).red = false; else if (pp.left == p) pp.left = r; else pp.right = r; r.left = p; p.parent = r; } return root; } //右旋轉 static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> l, pp, lr; if (p != null && (l = p.left) != null) { if ((lr = p.left = l.right) != null) lr.parent = p; if ((pp = l.parent = p.parent) == null) (root = l).red = false; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; }
代碼看上去比較繞,借用博客http://www.javashuo.com/article/p-orraekjs-n.html中的圖來解釋:
左旋的過程是將x
的右子樹繞x
逆時針旋轉,使得x
的右子樹成爲x
的父親,同時修改相關節點的引用。旋轉以後,二叉查找樹的屬性仍然知足。
右旋的過程是將x
的左子樹繞x
順時針旋轉,使得x
的左子樹成爲x
的父親,同時修改相關節點的引用。旋轉以後,二叉查找樹的屬性仍然知足。
針對HashMap的樹形結構的插入,刪除,查找操做也與數據結構中紅黑樹的操做是相似的,瞭解紅黑樹的操做也就瞭解了HashMap的樹形結構的操做,balanceInsertion和左旋右旋的操做是上述HashMap的樹形結構操做的關鍵。