原創做品,能夠轉載,可是請標註出處地址:http://www.javashuo.com/article/p-eygkcuto-a.htmlhtml
HashMap是基於哈希表實現的映射集合。 HashMap能夠擁有null鍵和null值,可是null鍵只能有一個,null值不作限制。HashTable是不容許null鍵和值的。 HashMap是非線程安全的集合,HashTable是添加了同步功能的HashMap,是線程安全的。 HashMap是無序的,並不能保證其內部鍵值對的順序。 HashMap提供了常量級複雜度的元素獲取和添加操做(固然是在hash分散均勻的狀況下)。 HashMap有兩個影響功能的因素:初始容量與負載因子,當集合中的元素數量超過了初始容量和負載因子的乘積值時,會觸發resize擴容 HashMap默認的初始容量是16,負載因子是0.75 HashMap在鏈表添加元素是採用尾插法,以前的版本採用頭插法,由於會致使循環鏈表的問題,改爲了尾插法,並添加了紅黑樹來優化鏈表過長的狀況下查詢慢的問題 HashMap底層結構爲數組+鏈表/紅黑樹 HashMap底層鏈表的元素達到8個的狀況下,若是HasnMap內部桶個數(即桶容量)達到64個則進行樹形化,不然進行resize擴容
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... // 默認的初始容量,值爲16,若是自定義也必須爲2的次冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量值 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的負載因子,值爲0.75,可自定義,必須小於1 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 鏈表轉樹結構的元素數量界限值,當某個hash值下的鏈表元素個數達到8個, // 則將其改成樹結構 static final int TREEIFY_THRESHOLD = 8; // 紅黑樹反轉鏈表的元素數量界限值,當數量小於6的時候纔會反轉 static final int UNTREEIFY_THRESHOLD = 6; // 樹形閾值,這個閾值針對的是整個Map中桶的數量,表示只有在所擁有的桶數量 // 達到64時才能執行樹形化,不然先去擴容去吧,可見在桶數小於64時,優先執行擴容 static final int MIN_TREEIFY_CAPACITY = 64; // 表示桶數組 transient Node<K,V>[] table; // 緩存 transient Set<Map.Entry<K,V>> entrySet; // 集合中元素的數量, transient int size; // 集合結構的修改次數,包括集合元素的增刪,和集合結構的變化,僅僅更改已有 // 元素的值並不會增長該值,主要用於Iterator的快速失敗 transient int modCount; // 集合的擴容閾值 int threshold; // 集合的負載因子,默認0.75,時間與空間的折中,增長負載因子, // 能增長元素容納量,減少空間消耗,卻增長的查詢的時間消耗, // 減少負載因子,能減小元素容納量,減小查詢時間消耗,但卻要及早的去擴容, // 增長了空間消耗 final float loadFactor; // ... }
添加新的映射元素(newKey,newValue),首先經過特定的hash算法計算newKey的hash值(newHash)。java
Hash算法:獲取newKey的hashCode值,而後進行高低位相異或。
hashCode值的獲取方法在Object類中已有定義,固然也有某些類進行了重寫,總的來講有如下幾種:node
- String類型的hashCode:自定義算法較複雜
- 包裝類型的hashCode:當前值
- 其餘類型的hashCode:類名+@+內存位置的16進製表示
若是是首次添加元素,那麼就意味着桶還沒有初始化,因此這裏會先執行初始化操做(resize),
若是初始化成功或者非首次添加元素,那麼開始定位元素的桶位算法
桶定位算法:用以前hash算法的結果newHash與桶的個數-1進行與操做
該算法的本意是保留newHash值的後幾進制位來肯定桶位,如何保留後幾位呢?咱們知道二進制算法中1的與操做具備保留原值的效果
這裏正是使用這種原理來實現,假設桶位數爲16位,16的二進制位10000,16-1=15,15的二進制位就是1111,末四位全是1,經過1的
保留原值的做用,當那它與newHash值的二進制值進行與操做後,結果就是newHash保留後4位的結果。而4位正好在桶位0-15以內。
而這也就是桶位數必須是2的次冪的緣由,由於2的次冪的數字的二進制值所有是首位爲1,其後全是0的值,當其-1以後就會變成首位
變0,其後全是1的值,而桶的下標是從0開始,最高位正好是-1以後的值。數組
查看確認桶位是否已有元素,若是沒有,直接存放新元素到該桶位,
若是桶位已有元素存在,那麼就是出現碰撞,這時的解決辦法就是使用鏈表或者紅黑樹來存儲,
若是該桶位存儲的數據結構已是紅黑樹,那麼執行紅黑樹添加元素操做,
不然執行鏈表的後插法,將新元素插入到鏈表的末尾。緩存
後插法:1.7以前的版本全是前插法,將新元素做爲表頭元素,1.8以後改爲後插法,將新元素做爲表尾元素
在執行後插法的時候須要遍歷鏈表,查找是否存在相同key的元素,若存在則直接用newValue替換舊值,再也不執行插入操做。安全
新元素插入完成以後,校驗Map中總元素個數是否達到了閾值(這個閾值是桶容量和負載因子的乘積),若是超過閾值則進行擴容。數據結構
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V put(K key, V value) { // 首先經過hash方法計算hash值,而後執行存值操做 return putVal(hash(key), key, value, false, true); } // hash算法:首先獲取key的hashCode,而後將其高低16位相異或,全員參與(hashcode值的全部二進制位都能參與hash運算) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 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; // 定位桶下標,n的值爲2的次冪,同時也是桶的數量,hash是以前經過hash算法得出的結果,n-1以後末幾位所有是1, // 再和hash與運算,等於保留hash的後幾位做爲結果。好比:(1111)&(01010101)的結果爲0101,保留了後四位 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)))) // 存在相同key的狀況(桶位置) e = p; 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); 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)))) // 存在相同key的狀況(鏈表元素位置) break; p = e; } } // 針對存在相同key的狀況進行統一處理:替換value 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; } //... }
注意:桶容量爲2的次冪的緣由正是由於便於元素經過位運算實現定位。多線程
執行擴容方法的緣由主要是集合中元素數量達到閾值或者是集合桶數組某個桶位置的元素數量達到8個,但集合桶容量未超過64的狀況下,
特殊的狀況是首次添加元素時的初始化操做也走這個方法。併發
針對初始化操做:
只會計算初始化容量和初始化閾值而後建立一個初始桶數組並返回結果。
對於使用了帶參構造器的狀況,會定製初始容量和負載因子,若是隻定製了初始容量則使用默認負載因子,
構造器會經過一個進制運算根據自定義的容量算出一個大於等於自定義容量值的最小的2的次冪值做爲真正的容量
好比:自定義容量爲10,則計算容量值爲16,而後再根據這個容量計算閾值爲12。
而針對擴容操做:
首先校驗舊容量是否已經達到或者大於容量最大值MAXIMUM_CAPACITY,若是是則再也不進行擴容操做,還在原桶數組中保存元素,
並將閾值設置爲Integer的最大值,設置爲最大值以後就不會再觸發擴容操做(由於Map中元素的總個數最大也就是Integer的最大值了,不可能比之更大),
而後校驗容量加倍後的新容量是否超過容量最大值MAXIMUM_CAPACITY,若是沒有的話則將閾值加倍。
新容量和新閾值都有了,而後建立新的桶數組,在以後就是元素遷移了。
元素遷移:
遍歷舊桶數組,校驗每一個桶位的元素結構,
若是隻有一個元素,直接在新桶數組進行重定位,定位方式不變,
若是是紅黑樹,走樹結構遷移邏輯,
不然就是鏈表,進行鏈表遷移,鏈表遷移進行了平衡優化,因爲新桶數組和舊數組的兩倍容量,
咱們簡單的將新容量分紅相等的兩半,稱之爲低位區與高位區,低位區下標與舊數組相同,
高位區下標爲舊數組下標+舊數組容量。
鏈表遷移時,會根據該鏈表中元素的鍵的hash值與舊容量進行與運算,這就會有兩個結果,爲0或者不爲0。
舊容量也是2的次冪,高位爲1,其後全是0,好比10000(表示容量爲16),將其和hash結果相與,
只會保留舊容量二進制爲1的那一位對應的hash值的那一位,其他位全變成0,若是hash值的那一
位爲0結果就是0,hash值那一位爲1結果就是10000。
根據相與的結果來進行鏈表分拆,將結果爲0的鏈表元素還定位到相同的桶下標位,即新桶數組的低位區,將結果爲1的鏈表元素定位到原下標+舊桶容量的位置,即高位區。
這兩個鏈表會先組裝鏈表結構,而後將鏈表表頭元素定位到低位區或高位區。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;// 舊容量 int oldThr = threshold;// 舊閾值 int newCap, newThr = 0;// 新容量、新閾值 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { // 若是舊容量已經達到最大容量值,將閾值設置爲最大值,返回舊桶數組 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) // initial capacity was placed in threshold // 只在經過帶參數的構造器(初始容量和負載因子) // 建立的容器首次添加元素進行桶數組初始化時會走這裏 newCap = oldThr; else { // zero initial threshold signifies using defaults // 初始化容量和閾值,這就是首次添加元素時執行的初始化邏輯 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 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; // 若是是初始化操做,此處oldTab爲null,會直接返回新建桶數組,不然執行元素遷移 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) // 針對桶位置只有一個元素的狀況,直接重定位元素,定位模式一致 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 針對紅黑樹的狀況 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order 針對鏈表的狀況 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do {// 遍歷舊桶位的舊鏈表 next = e.next; // 這個判斷的結果取決於在於hash值在於oldCap的1所在進制位對應的進制位是1仍是0, // 因爲oldCap只有這一位爲1,那麼hash的該位將保留原值,其他位所有得0,增長這麼 // 一個貌似隨機的判斷,用於進一步分散元素到不一樣的桶。 // 其實就是將舊桶第i位桶的鏈表元素分散到新桶的第i和第i+oldCap桶位上,爲0仍是爲1隨機 // 在循環中造成兩個小鏈表,而後將首個元素賦值給新桶的對應桶位便可。 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); // 定位兩個小鏈表的首元素 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } //... }
簡單看代碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V get(Object key) { Node<K,V> e; // 計算key的hash值用做桶定位的基礎 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) { // 只有一個元素、鏈表頭元素、樹根元素要單獨校驗 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 { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } //... }
簡單看源碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V remove(Object key) { Node<K,V> e; // 計算key的hash值,用做後面定位元素桶位的基礎 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 { // 鏈表循環獲取等key的元素 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; } //... }
參照源碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... // 樹形化準備 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 對於觸發了樹形化操做,可是桶容量還沒達到64的狀況下優先去作擴容處理,擴容也會分拆鏈表 resize(); // 定位要作樹形下的桶位置,獲取桶位元素e else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 循環遍歷鏈表中的元素,將其改形成爲雙向鏈表結構,表頭元素爲hd do { // 將e元素封裝成爲樹節點TreeNode 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); } } // 將Node節點封裝成樹節點 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 樹形化操做 final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null;//表明根節點 // 此處循環將this賦值給x,this表明的是當前樹節點,這個類是HashMap的內部類用於標識樹節點, // this就是當前類的實例,也就是一個樹節點,可是是哪一個樹節點,就要依靠之間的代碼上下文來判 // 斷了,看看調用該方法的地方有這樣的代碼:hd.treeify(tab);這就表示當前節點就是那額hd節 // 點,而這個hd節點就是以前改造好的雙向鏈表的表頭結點 // 這裏循環的是雙向鏈表中的元素 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) { // root == null的狀況是鏈表頭結點的時候纔會出現,這時候將這個頭結點做爲樹根節點 x.parent = null;//根節點無父節點 x.red = false;//黑色 root = x;//賦值 } else { // 這裏只有非鏈表頭節點才能進來 K k = x.key; int h = x.hash; Class<?> kc = null; // 此處循環的是已構建的紅黑樹的節點,從根節點開始,遍歷比較當前鏈表節點與當前紅黑樹節點的 // hash值,dir用於保存比較結果,若是當前鏈表節點小,則dir爲-1,不然爲1,實際狀況倒是,能 // 撥到同一個桶位的全部元素的hash值那是同樣的呀,因此dir的值是沒法依靠hash值比較得出結果 // 的,那麼重點就靠最後一個條件判斷來得出結果了, 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);// 最後須要依靠這個方法來決定dir的值 TreeNode<K,V> xp = p; // 根據dir的值來決定將當前鏈表節點保存到當前樹節點的左邊仍是右邊, // 或者當前鏈表節點須要與當前樹節點的左節點仍是右節點接着比較 // 主要尋找子節點爲null的狀況,將節點保存到null位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) // dir<=0,將鏈表節點保存到當前樹節點的左邊子節點位置 xp.left = x; else // dir<=0,將鏈表節點保存到當前樹節點的右邊子節點位置 xp.right = x; // 一旦添加的一個新節點,就要進行樹平衡操做,以此保證紅黑樹結構 // 樹的平衡操做依靠的就是其左右旋轉操做 root = balanceInsertion(root, x); break; } } } } // 最後將組裝好的樹的根節點保存到桶下標位 moveRootToFront(tab, root); } static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { // 首先定位桶下標位 int index = (n - 1) & root.hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; // 校驗當前桶下標位的值是否爲根節點的值,可能會存在不一樣的緣由是樹的平衡操做將本來的根節點挪移了 // 若是相同,那麼不做任何處理,若是不一樣,就須要替換桶位元素爲樹根節點元素,而後改變雙向鏈表結構 // 將root根節點做爲雙向鏈表表頭元素,爲什麼要替換呢,由於在判斷桶位元素類型時會對鏈表進行遍歷,如 // 果桶位置放的不是鏈表頭或者尾元素,遍歷將變得很是麻煩 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); } } //... } //... }
很簡單,看源碼
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 將一顆大樹分拆爲兩顆小樹,若是樹過小,退化爲單向鏈表 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { // this表明當前節點,也就是樹的根節點,桶位節點 // map表明當前集合 // tab表明新桶數組 // index表明當前節點的桶位下標 // bit爲舊桶容量 TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0;// lc表示低位樹容量,hc表示高位樹容量 for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; // 分拆樹節點的依據,結果爲0的一組(低位組),結果不爲0的一組(高位組) 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個則退化爲單向鏈表 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); } } // 針對高位組進行樹形化處理,若是該組元素少於6個則退化爲單向鏈表 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 HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 紅黑樹的添加元素,map爲當前HashMap,tab爲當前桶數組,h爲新增元素的key的hash值,k爲新增元素的key,v爲新增元素的value 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 (TreeNode<K,V> p = root;;) { // dir表明當前樹節點與待添加節點的hash比較結果 // ph表明當前樹節點的hash值 // pk表明當前樹節點的key // 因爲一個桶位的全部元素hash值相等,因此最後得出結果須要依靠 int dir, ph; K pk; if ((ph = p.hash) > h) // 若是當前節點的hash值大,dir爲-1 dir = -1; else if (ph < h) // 若是當前節點的hash值小,dir爲1 dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk))) // hash值相等的狀況下,若是key也同樣直接返回當前節點,返回去以後會執行value的替換操做 return p; 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)) // 這個找到的q也是與待添加元素key相同的元素,執行替換 return q; } // 最終須要依靠這個方法來得出dir值的結果 dir = tieBreakOrder(k, pk); } TreeNode<K,V> xp = p; // 根據dir的值來決定是當前節點的左側仍是右側,若是該側右子節點則繼續循環尋找位置,不然直接將新元素添加到該側子節點位置 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);//封裝樹節點 if (dir <= 0) // dir<=0,將新節點添加到當前節點左側 xp.left = x; else // 不然將新節點添加到當前節點右側 xp.right = x; // 設置新節點的鏈表位置,將其做爲xp的下級節點 xp.next = x; x.parent = x.prev = xp; if (xpn != null) // 若是xp節點本來有下級節點xpn,則要將新節點插入到xp和xpn之間(指雙向鏈表中) ((TreeNode<K,V>)xpn).prev = x; // 插入了新節點以後,要進行樹平衡操做,平衡操做完成,將根節點設置爲桶位節點 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } // 通常咱們在HashMap中保存的鍵值對的類型都是不變的,這通常用泛型控制, // 那麼就意味着,兩個元素的key的類型時同樣的,因此才須要靠其hashCode來決定大小 // System.identityHashCode(parameter)是本地方法,用於獲取和hashCode同樣的結果, // 這裏的hashCode指的是默認的hashCode方法,與某些類重寫的無關 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; } //... } //... }
繞A節點左旋,等於將A的右子節點B甩上來替換本身的位置,而本身順勢下沉成爲其左子節點,這時你會發現,B有三個子節點,明顯結構不對,將B的原來的左子節點C轉移到下沉的A上,成爲其右子節點,旋轉結束
其實,要保證左子樹節點值小於其根節點,右子樹節點值大於其根節點,那麼在替換AB節點以後,C節點的值就出現了問題,只有將其挪到A節點右邊才能繼續保證上面的結構。
首先咱們知道B節點爲A的右節點,那麼B>A,而C爲B的左節點,則C<B,而C又位於A的右子樹,則C>A,所以:A<C<B。要保證這個式子永遠成立,就必須依靠挪移節點來完成。
如今B爲最頂部節點且爲最大值,那麼A和C必須位於其左子樹,而C>A則,C必須位於A的右子樹,再看看以前的狀況,若是A爲頂點節點,那麼BC均應位於其右子樹,而B>C,那麼要麼B爲C的右節點,要麼C爲B的左節點
繞A幾點右旋,等於將A的左子節點B甩上來替換本身的位置,而本身順勢下沉成爲其右子節點,這是你會發現,B有三個子節點,明顯結構不對,將B的原來的右子節點C轉移到下沉的A上,成爲其左子節點,旋轉結束
首先咱們知道B爲A的左子節點,因此B<A,再者C爲B的右子節點,那麼C>B,而C又位於A的左子樹,則C<A,最後:A>C>B。要保證這個結果成立,那麼再B替換A的位置以後,A下沉爲B的右子節點,由於A>B,因此往右走,
這時C和A均位於B的右側,比較兩者發現C<A,那麼將C放到A的左側成爲其左子節點
新增節點所有初始化爲紅色節點,而後分如下幾種狀況:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 左旋操做,其中root爲根節點,p爲當前節點,r爲p的右節點,rl爲r的左節點,pp爲p的父節點 // 左旋以後,r替換p的位置,rl挪到p的右節點 // 節點位置變換以後,既要改變其父節點的left/right值,也要改變當前節點中parent的值, // 改變是雙向的,父子均有指向,改變以後均要修改 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) { // 首先將r節點的左子節點(rl)送給p當其右子節點 if ((rl = p.right = r.left) != null) rl.parent = p;//變換rl的父節點爲p,原來爲r if ((pp = r.parent = p.parent) == null) // 原p節點爲根節點的狀況,r替換以後,須要從新着色爲黑色,保證根節點爲黑色 (root = r).red = false; else if (pp.left == p) // 原p節點爲其父節點pp的左子節點的狀況,r替換後,須要修改pp節點的left指向r節點 pp.left = r; else // 原p節點爲其父節點pp的右子節點的狀況,r替換後,須要修改pp節點的right指向r節點 pp.right = r; //而後將p節點做爲r節點的左子節點,即爲p節點順勢下沉爲r的左子節點 r.left = p; p.parent = r;//變換p的父節點爲r } return root; } // 右旋操做,嘿,那就是左旋的反向操做罷了 // root爲根節點,p爲當前節點,l爲其左子節點,lr爲l的右子節點,pp爲p的父節點 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) { // 首先將l的右子節點lr挪給p if ((lr = p.left = l.right) != null) lr.parent = p;//變換lr的父節點爲p,原來爲l if ((pp = l.parent = p.parent) == null) // 若是p節點是根節點,替換爲l以後,l便成爲新的根節點,須要從新着色爲黑色,保證紅黑樹結構 (root = l).red = false; else if (pp.right == p) // 若是原p節點是其父節點pp的右子節點,那麼須要將其右子節點改爲l pp.right = l; else // 若是原p節點是其父節點pp的左子節點,那麼須要將其左子節點改爲l pp.left = l; // 最後將原p節點置爲l節點的右子節點,並修改p的父節點爲l l.right = p; p.parent = l; } return root; } // 平衡操做,x爲新增節點,root爲根節點 static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { x.red = true;// 新增節點所有爲紅色節點 for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { if ((xp = x.parent) == null) { // 1 x爲根節點的狀況,將其從新着色爲黑色 x.red = false; return x; } else if (!xp.red || (xpp = xp.parent) == null) // 2 若是x節點的父節點爲黑色,又或者x的父節點是根節點,沒有影響,不操做 return root; if (xp == (xppl = xpp.left)) { // 3 若是x節點的父節點是其父節點(x的祖父節點)的左子節點 if ((xppr = xpp.right) != null && xppr.red) { // 3-1 再若是x的祖父節點的右子節點存在且爲紅色,則將這個節點和x的父節點通通改爲黑色, // 再把x的祖父節點改爲紅色,將x祖父節點做爲新的x節點執行循環 xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } else { // 3-2 不然的狀況 if (x == xp.right) { // 3-2-1 若是x節點是其父節點的右子節點,則執行以x父節點爲基準的左旋操做, // 左旋以後新增節點x替了其原父節點xp,將原xp節點當作如今的x節點,原來的x // 節點是如今x節點的父節點xp,原來的x節點的祖父節仍是如今x的祖父節點 root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) {// xp爲原來的x節點 // 將xp節點置爲黑色 xp.red = false; if (xpp != null) {// xpp仍是以前的xpp // 將xpp節點置爲紅色,而後執行右旋,右旋能夠將xpp節點用xp節點替換,紅黑交換 xpp.red = true; root = rotateRight(root, xpp); } } } } else { // 4 若是x節點的父節點是其父節點(x的祖父節點)的右子節點 if (xppl != null && xppl.red) { // 4-1 再若是x的祖父節點的左子節點存在而且爲紅色,則將該節點置爲黑色, // 將x的父節點置爲黑色,祖父節點置爲紅色,而後把xpp祖父節點做爲新的x節點 xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } else { // 4-2 不然的狀況 if (x == xp.left) { // 4-2-1 若是x節點是其父節點的左子節點的狀況,先以x父節點進行右旋, // 右旋以後原來的xp節點被新的x節點替換,原來的xp節點做爲新xp節點的右子節點, // 如今看做爲x,而後從新定義xpp,其實xpp位置不變 root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { // 將如今的xp節點置爲黑色 xp.red = false; if (xpp != null) { // 將祖父節點置爲紅色。而後執行左旋,左旋以後,原來的xp節點接替了xpp節點的位置,xpp變成原來xp的左子節點 xpp.red = true; root = rotateLeft(root, xpp); } } } } } } //... } //... }
紅黑樹的節點刪除操做主要分爲這麼三種:
針對第一種狀況,真的好簡單,待刪節點即爲葉子節點,直接刪除便可;
針對第二種狀況,也不難,將那個子節點替換待刪節點便可;
至於第三種狀況,那就麻煩了,但經過變換,能夠將其轉化爲第一種或者第二種狀況:處理方式是,找到待刪節點的右子樹中的最左節點(或者左子樹中的最右節點),將其與待刪節點互換位置,而後就將狀況轉換爲第一種或者第二種了。
針對第三種狀況轉換方法的解析:爲何要找到待刪節點的右子樹最左節點呢,由於紅黑樹是二叉搜索樹,這個二叉搜索樹中知足"左子節點<其父節點<其父節點的右子節點"的規則,那麼找到的右子樹的最左節點,就是整顆樹中大於待刪節點值的最小值節點了,爲了保證二叉搜索樹的搜索結構(也就是剛剛那個公式),咱們只能找最接近待刪節點值的節點值來接替它的位置,如此能保證二叉搜索的結構,可是可能會破壞紅黑樹的結構,由於若是待刪節點爲紅色,而替換節點爲黑色的話,那豈不是在待刪節點分支多加了一個黑色節點嘛,還有其餘各類狀況,種種,須要進行刪除節點後的樹平衡操做來保證紅黑樹的結構完整。
下面重點說說刪除後的平衡問題:
其實只要待刪節點是黑色節點,一旦刪除必然會致使分支中黑色節點缺一(紅黑樹再也不平衡),具體狀況又分爲如下幾種:(基礎條件:待刪節點p爲黑色,其只有一個子節點x,操做在待刪節點被刪除以後,子節點替換其位置以後)
貌似有點難...你們要看進去思考才能理解,光看沒用!
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 當前節點即爲要刪除的節點,map爲當前集合,tab爲當前桶數組 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { int n; if (tab == null || (n = tab.length) == 0) return; // 定位待刪節點的桶位下標index int index = (n - 1) & hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; if (pred == null) // 若是當前節點是雙向鏈表頭節點/樹根節點,則將鏈表第二元素置爲桶位元素,即刪除該節點 tab[index] = first = succ; else // 若是當前節點不是雙向鏈表頭節點,則將其後置節點賦給其前置節點做爲後置節點,即刪除該節點 pred.next = succ; if (succ != null) // 修改後置節點中prev指向新的前置元素節點 succ.prev = pred; if (first == null) return; if (root.parent != null) root = root.root(); if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { // 退化爲鏈表機構 tab[index] = first.untreeify(map); // too small return; } //// 以前的操做是在雙向鏈表中刪除當前節點的痕跡,下面是在樹結構中刪除的操做 // p爲待刪節點(即當前節點),pl爲p的左子節點,pr爲p的右子節點, TreeNode<K,V> p = this, pl = left, pr = right, replacement; if (pl != null && pr != null) { // 當前節點同時擁有左右子節點的狀況,sl表示當前節點的右子樹的最左節點 // 要刪除當前節點,須要找到與當前節點值最靠近的左右兩側的節點之一,這 // 裏找的是右側的,即找的是整個樹中大於當前節點值的最小值節點,將找到 // 的節點與待刪節點互換,互換以後再刪除節點,若是原來的那個最左節點還 // 有右子節點,則將該右子節點替換其父節點(待刪節點) TreeNode<K,V> s = pr, sl; while ((sl = s.left) != null) // find successor s = sl;// 找到右子樹的最左節點 boolean c = s.red; s.red = p.red; p.red = c; // swap colors 首先互換顏色 TreeNode<K,V> sr = s.right;// s爲最左節點,那麼它不可能有左子節點,最多有右子節點 TreeNode<K,V> pp = p.parent; if (s == pr) { // p was s's direct parent // 若是找到的s即爲待刪節點的直接右子節點(說明s無左子節點),那麼直接替換這兩個節點 p.parent = s; s.right = p; } else { // 不然的狀況,先找到s的父節點sp,將其設置爲p的父節點, TreeNode<K,V> sp = s.parent; if ((p.parent = sp) != null) { if (s == sp.left) // 將p做爲原來s的父節點的左子節點(即替換p和s的位置) sp.left = p; else // TODO 這裏是什麼意思呢?找的就是sp的最左節點,這裏怎麼跑到右節點上去了呢,雖然p是要刪除的節點 sp.right = p; } // 把p的右子節點pr置爲s的右子節點 if ((s.right = pr) != null) // 把s置爲pr的父節點 pr.parent = s; } // 替換以後p是無左子節點的,(即原來的s是最左節點,無左子節點) p.left = null; // 把s的右子節點sr置爲p的右子節點 if ((p.right = sr) != null) // 把sr的父節點設置爲p sr.parent = p; if ((s.left = pl) != null) // 將p的左子節點置爲s的左子節點 pl.parent = s; // 把p的父節點設置爲s的父節點 if ((s.parent = pp) == null) // 若是p沒有父節點,將s節點設置爲根節點 root = s; // 不然若是p是其父節點pp的左子節點 else if (p == pp.left) // 如今將s設置爲pp的左子節點 pp.left = s; else // 不然若是p是其父節點的右子節點,則將s設置爲pp的右子節點 pp.right = s; if (sr != null) // 若是s存在右子節點,則將其置爲replacement,如今和待刪節點只有右子節點的狀況同樣 replacement = sr; else // 不然將p置爲replacement,至此第一種狀況替換結束,如今和待刪節點沒子節點的狀況同樣 replacement = p; } else if (pl != null) // 待刪節點只有左子節點的狀況,將其左子節點置爲replacement replacement = pl; else if (pr != null) // 當前節點只有右子節點的狀況,將其右子節點置爲replacement replacement = pr; else // 待刪節點沒有子節點的狀況,直接將其設置爲replacement replacement = p; if (replacement != p) {// 若是待刪節點有子節點replacement的狀況 // 準備替換replacement節點和p節點 TreeNode<K,V> pp = replacement.parent = p.parent; if (pp == null) // 待刪節點p爲根節點的狀況,將replacement設置爲根節點便可 root = replacement; else if (p == pp.left) // p是做爲其父節點pp的左子節點,則將replacement設置爲pp的左子節點 pp.left = replacement; else // 不然p是做爲其父節點pp的右子節點,則將replacement設置爲pp的右子節點 pp.right = replacement; // 最後將p節點的全部關係置空 p.left = p.right = p.parent = null; } // 若是待刪節點是紅色節點則不影響平衡,無需執行樹平衡操做,不然須要進行樹平衡操做 TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); // 若是p節點沒有任何子節點的狀況 if (replacement == p) { // detach // 根據實際狀況置空p節點的各屬性 TreeNode<K,V> pp = p.parent; p.parent = null; if (pp != null) { if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } } if (movable) // 若是可移動,那麼將根節點設置爲桶位節點 moveRootToFront(tab, r); } // 刪除節點後的平衡操做,root爲根節點,x爲上面提到的replacement節點,該節點其實爲替換p節點,爲其子節點 static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { for (TreeNode<K,V> xp, xpl, xpr;;) { if (x == null || x == root) // 若是x不存在或者其原本就是根節點,字節返回根節點 return root; else if ((xp = x.parent) == null) { // 若是x節點不存在父節點,那麼則其爲根節點,可是還未設置爲root節點,那麼直接置黑色,並將其設置爲root節點 x.red = false; return x; } else if (x.red) { // 若是x是紅色節點,則將其置黑,由於若是x爲紅色,那麼其父節點p必然爲黑色, // 刪掉以後,會致使黑色節點減小,正好x爲紅色拿來補充黑色節點,使黑色節點數不變, // 若是x是黑色,那麼就會致使當前分支黑色節點減小,須要使用其餘方法進行平衡 x.red = false; return root; } // 若是x爲其父節點左子節點(刪除後的結果) else if ((xpl = xp.left) == x) { // 若是x存在兄弟節點(其父節點的右子節點),且爲紅色,那麼xp一定爲黑色 // 那麼就代表x節點分支的黑色節點少了一個,也就是其兄弟節點多一個(其餘全部分支都多一個), if ((xpr = xp.right) != null && xpr.red) { // 那麼將兄弟節點置黑,父節點置紅,這是x分支仍是少一個黑節點,兄弟分支黑節點不變 xpr.red = false; xp.red = true; // 再而後執行xp節點的左旋,將其右子節點(即x的兄弟節點)甩上去, // xp做爲其左子節點,如此一來將兄弟節點這一黑色幾點變成兩份之共享, // 無形之中使得x分支黑色節點加1,從而達到平衡 root = rotateLeft(root, xp); xpr = (xp = x.parent) == null ? null : xp.right; } if (xpr == null) x = xp;// 若是x沒有兄弟節點,那麼循環以x的父節點爲基準再次進行平衡 else { // 不然,那就是x存在兄弟節點,且爲黑色的狀況,則其父節點顏色不明,x顏色不明 // sl爲兄弟節點的左子節點,sr爲兄弟節點右子節點 TreeNode<K,V> sl = xpr.left, sr = xpr.right; // 若是sr和sl中,所有爲null,或者所有爲黑色節點,或者有一個爲黑色節點,另外一個是null if ((sr == null || !sr.red) && (sl == null || !sl.red)) { // 將兄弟節點置爲紅色 xpr.red = true; x = xp;// 爲了循環 } // 不然,就是sl和sr全爲紅色或者一紅一null,或者一紅一黑 else { // 若是sr爲null(則sl必爲紅色),或者sr爲黑色(則sl必爲紅色) if (sr == null || !sr.red) { if (sl != null) // 將sl置爲黑色 sl.red = false; // 兄弟節點置紅 xpr.red = true; // 而後右旋兄弟節點,將其左子節點甩上去作本身的父節點,這時兄弟分支黑節點數量不變 root = rotateRight(root, xpr); xpr = (xp = x.parent) == null ? null : xp.right;// 更新xpr的指向,將其指向旋轉以後新的兄弟節點,即原來的sl(黑色) } if (xpr != null) { // 將xp和xpr顏色弄一致,由於若是xp是黑色,左旋以後兄弟分支會少一個黑節點, // 這樣xpr就會補充這個黑色,若是xp是紅色,那麼xpr也是紅色,左旋以後xpr落座 // xp的位置,仍是原來的顏色,而左側確多出了xp這個黑色節點。 xpr.red = (xp == null) ? false : xp.red; if ((sr = xpr.right) != null) // 將sr置黑即把原來的xpr置黑 sr.red = false; } if (xp != null) { // 將xp置黑 xp.red = false; // 而後在執行xp左旋,等於將sl甩到了xp的位置, // 並且這個sl必然爲黑色,是爲了補充x分支缺乏的那一個黑節點 root = rotateLeft(root, xp); } x = root; } } } // 不然若是x是其父節點的右子節點的話 else { // symmetric (對稱的) if (xpl != null && xpl.red) { xpl.red = false; xp.red = true; root = rotateRight(root, xp); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl == null) x = xp; else { TreeNode<K,V> sl = xpl.left, sr = xpl.right; if ((sl == null || !sl.red) && (sr == null || !sr.red)) { xpl.red = true; x = xp; } else { if (sl == null || !sl.red) { if (sr != null) sr.red = false; xpl.red = true; root = rotateLeft(root, xpl); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl != null) { xpl.red = (xp == null) ? false : xp.red; if ((sl = xpl.left) != null) sl.red = false; } if (xp != null) { xp.red = false; root = rotateRight(root, xp); } x = root; } } } } } //... } //... }
HashMap中涉及到了數組操做,單向鏈表操做,雙向鏈表操做,紅黑樹操做:
數組操做:
- HashMap中的數組指的就是桶數組,用於承載鏈表(的頭結點)和紅黑樹(根節點)
- 數組的建立在第一次往集合中添加元素的時候進行,默認的數組長度爲16,也能夠手動指定,系統會自動計算一個大於等於給定容量的最小的2的次冪的容量值做爲數組初始容量,數組的最大容量爲不超過兩倍的Integer的最大限值。數組還有一個負載因子,默認爲0.75,這個值可算是一個時間空間的折中了,通常不會手動修改,但也能手動指定,若是設置過大,查詢消耗增長,若是設置太小,空間消耗增長。
- 當向集合中添加一個新元素的時候,經過元素的key的hash算法結果來定位其在數組中的位置,這個hash算法要選擇的足夠好來使得元素可以儘可能平均的散佈在數組的各個位置,而不是堆積在幾處。
- 數組的容量是不可變的,因此一旦原數組容量受限,通常是建立新的數組來替代,這叫數組的擴容,擴容須要知足必定的條件,HashMap中有一個閾值用於做爲擴容的條件,這個值是當前容量和負載因子的乘積,只要當前的集合的元素數量達到了閾值,就要執行擴容操做,固然還有一種狀況也要擴容,那就是在單個數組位上的元素數量達到8個時,但數組容量未達到64個時,優先執行數組擴容。數組擴容後新數組爲原數組的2倍容量。
單向鏈表操做:
- hashMap是依靠hash來存儲元素的,hash存儲老是難以免碰撞的出現,HashMap使用單向鏈表來保存發生碰撞的元素。新元素會保存到鏈表的尾部,若是新元素的key已經存在,那麼將會是一個更新操做,不會有新元素增長。
- 數組擴容的時候須要進行元素的遷移,這裏就是鏈表的遷移,鏈表遷移的時候會觸發鏈表分拆,將一個完整鏈表分拆成爲兩個鏈表,咱們成爲低位鏈表和高位鏈表,低位鏈表的數組位置同舊數組,而高位鏈表的數組位置爲低位鏈表數組位+舊數組容量。可見經過鏈表分拆也是能夠下降鏈表中元素數量的。
- 在Jdk1.8之前的版本中在高併發的狀況下,HashMap數組擴容的時候可能會出現死循環,這是由於兩個線程同時進行擴容和元素遷移致使出現了循環鏈表,Jdk1.8中已經修復這一Bug,採用的是將頭插法改成尾插法,可是HashMap仍然是線程不安全的集合,多線程環境中最好使用ConcurrentHashMap。
雙向鏈表操做:
- hashMap中存在雙向鏈表,他是在單向鏈表元素達到8個,且數組容量達到64位以後執行樹形化時轉換的,也就是說,HashMap中的紅黑樹同時也是一個雙向鏈表。這個雙向鏈表的做用是在某些特殊狀況下(樹過小的時候),在將紅黑樹退化爲單向鏈表結構時使用的。正由於如此,在紅黑樹的增刪節點、平衡節點的時候還須要保證雙向鏈表的結構。
紅黑樹操做:
- 這一部分能夠算是hashMap中最最複雜難懂的東西了
- 紅黑樹的樹形化操做
- 紅黑樹的增長元素操做
- 紅黑樹的增長元素平衡操做
- 紅黑樹的樹分拆操做
- 紅黑樹的刪除元素操做
- 紅黑樹的刪除元素平衡操做
- 紅黑樹的退化單項鍊表操做,有兩處退化,一處是在擴容時,樹分拆以後,子樹內元素容量少於6個時,執行退化操做,還有就是在移除樹中元素以後,若是樹結構知足某些條件則執行退化操做
- 其實平衡的目的就是爲了恢復被添加和移除元素操做破壞的紅黑樹結構罷了,使用的手段無非變色和旋轉。