做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
在上一章節咱們講解並用數據驗證了,HashMap中的,散列表的實現
、擾動函數
、負載因子
以及擴容拆分
等核心知識點以及相應的做用。java
除了以上這些知識點外,HashMap還有基本的數據功能;存儲
、刪除
、獲取
、遍歷
,在這些功能中常常會聽到鏈表、紅黑樹、之間轉換等功能。而紅黑樹是在jdk1.8引入到HashMap中解決鏈表過長問題的,簡單說當鏈表長度>=8
時,將鏈表轉換位紅黑樹(固然這裏還有一個擴容的知識點,不必定都會樹化[MIN_TREEIFY_CAPACITY])。node
那麼本章節會進行講解如下知識點;程序員
🕵注意: 建議閱讀上一篇後,再閱讀本篇文章《HashMap核心知識,擾動函數、負載因子、擴容鏈表拆分,深度學習》面試
經過上一章節的學習:《HashMap核心知識,擾動函數、負載因子、擴容鏈表拆分,深度學習》 數組
你們對於一個散列表數據結構的HashMap往裏面插入數據時,基本已經有了一個印象。簡單來講就是經過你的Key值取得哈希再計算下標,以後把相應的數據存放到裏面。微信
但再這個過程當中會遇到一些問題,好比;數據結構
這些疑問點都會在後面的內容中逐步講解,也能夠本身思考一下,若是是你來設計,你會怎麼作。app
HashMap插入數據流程圖函數
visio原版流程圖,能夠經過關注公衆號:bugstack蟲洞棧,進行下載
以上就是HashMap中一個數據插入的總體流程,包括了;計算下標、什麼時候擴容、什麼時候鏈表轉紅黑樹等,具體以下;
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
判斷tab是否位空或者長度爲0,若是是則進行擴容操做。
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
tab[i = (n - 1) & hash])
treeifyBin(tab, hash);
threshold
,超過則擴容。treeifyBin
,是一個鏈表轉樹的方法,但不是全部的鏈表長度爲8後都會轉成樹,還須要判斷存放key值的數組桶長度是否小於64 MIN_TREEIFY_CAPACITY
。若是小於則須要擴容,擴容後鏈表上的數據會被拆分散列的相應的桶節點上,也就把鏈表長度縮短了。JDK1.8 HashMap的put方法源碼以下:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 初始化桶數組 table,table 被延遲到插入新數據時再進行初始化 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; // 若是鍵的值以及節點 hash 等於鏈表中的第一個鍵值對節點時,則將 e 指向該鍵值對 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若是桶中的引用類型爲 TreeNode,則調用紅黑樹的插入方法 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; } // 條件爲 true,表示當前鏈表包含要插入的鍵值對,終止遍歷 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 判斷要插入的鍵值對是否存在 HashMap 中 if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent 表示是否僅在 oldValue 爲 null 的狀況下更新鍵值對的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 鍵值對數量超過閾值時,則進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap是基於數組+鏈表和紅黑樹實現的,但用於存放key值得的數組桶的長度是固定的,由初始化決定。
那麼,隨着數據的插入數量增長以及負載因子的做用下,就須要擴容來存放更多的數據。而擴容中有一個很是重要的點,就是jdk1.8中的優化操做,能夠不須要再從新計算每個元素的哈希值,這在上一章節中已經講到,能夠閱讀系列專題文章,機制以下圖;
裏咱們主要看下擴容的代碼(註釋部分);
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // Cap 是 capacity 的縮寫,容量。若是容量不爲空,則說明已經初始化。 if (oldCap > 0) { // 若是容量達到最大1 << 30則再也不擴容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 按舊容量和閥值的2倍計算新容量和閥值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold // initial capacity was placed in threshold 翻譯過來的意思,以下; // 初始化時,將 threshold 的值賦值給 newCap, // HashMap 使用 threshold 變量暫時保存 initialCapacity 參數的值 newCap = oldThr; else { // zero initial threshold signifies using defaults // 這一部分也是,源代碼中也有相應的英文註釋 // 調用無參構造方法時,數組桶數組容量爲默認容量 1 << 4; aka 16 // 閥值;是默認容量與負載因子的乘積,0.75 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr爲0,則使用閥值公式計算容量 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"}) // 初始化數組桶,用於存放key Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 若是舊數組桶,oldCap有值,則遍歷將鍵值映射到新數組桶中 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) // 這裏split,是紅黑樹拆分操做。在從新映射時操做的。 ((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; // 這裏是鏈表,若是當前是按照鏈表存放的,則將鏈表節點按原順序進行分組{這裏有專門的文章介紹,如何不須要從新計算哈希值進行拆分《HashMap核心知識,擾動函數、負載因子、擴容鏈表拆分,深度學習》} do { next = e.next; 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; }
以上的代碼稍微有些長,可是總體的邏輯仍是蠻清晰的,主要包括;
new Node[newCap];
HashMap這種散列表的數據結構,最大的性能在於能夠O(1)時間複雜度定位到元素,但由於哈希碰撞不得已在一個下標裏存放多組數據,那麼jdk1.8以前的設計只是採用鏈表的方式進行存放,若是須要從鏈表中定位到數據時間複雜度就是O(n),鏈表越長性能越差。由於在jdk1.8中把過長的鏈表也就是8個,優化爲自平衡的紅黑樹結構,以此讓定位元素的時間複雜度優化近似於O(logn),這樣來提高元素查找的效率。但也不是徹底拋棄鏈表,由於在元素相對很少的狀況下,鏈表的插入速度更快,因此綜合考慮下設定閾值爲8才進行紅黑樹轉換操做。
鏈表轉紅黑樹,以下圖;
以上就是一組鏈表轉換爲紅黑樹的狀況,元素包括;40、5一、6二、7三、8四、9五、150、161 這些是通過實際驗證可分配到Idx:12的節點
經過這張圖,基本能夠有一個鏈表
換行到紅黑樹
的印象,接下來閱讀下對應的源碼。
鏈表樹化源碼
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 這塊就是咱們上面提到的,不必定樹化還可能只是擴容。主要桶數組容量是否小於64 MIN_TREEIFY_CAPACITY if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // 又是單詞縮寫;hd = head (頭部),tl = tile (結尾) 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); } }
這一部分鏈表樹化的操做並不複雜,複雜點在於下一層的紅黑樹轉換上,這部分知識點會在後續章節中專門介紹;
以上源碼主要包括的知識點以下;
tl.next = p
,這主要方便後續樹轉鏈表和拆分更方便。tieBreakOrder
加時賽,這主要是由於HashMap沒有像TreeMap那樣自己就有Comparator的實現。在鏈表轉紅黑樹中咱們重點介紹了一句,在轉換樹的過程當中,記錄了原有鏈表的順序。
那麼,這就簡單了,紅黑樹轉鏈表時候,直接把TreeNode轉換爲Node便可,源碼以下;
final Node<K,V> untreeify(HashMap<K,V> map) { Node<K,V> hd = null, tl = null; // 遍歷TreeNode for (Node<K,V> q = this; q != null; q = q.next) { // TreeNode替換Node Node<K,V> p = map.replacementNode(q, null); if (tl == null) hd = p; else tl.next = p; tl = p; } return hd; } // 替換方法 Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { return new Node<>(p.hash, p.key, p.value, next); }
由於記錄了鏈表關係,因此替換過程很容易。因此好的數據結構可讓操做變得更加容易。
上圖就是HashMap查找的一個流程圖,仍是比較簡單的,同時也是高效的。
接下來咱們在結合代碼,來分析這段流程,以下;
public V get(Object key) { Node<K,V> e; // 一樣須要通過擾動函數計算哈希值 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 && // 計算下標,哈希值與數組長度-1 (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) { // TreeNode 節點直接調用紅黑樹的查找方法,時間複雜度O(logn) 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; }
以上查找的代碼仍是比較簡單的,主要包括如下知識點;
tab[(n - 1) & hash])
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) { Node<K,V>[] tab; Node<K,V> p; int n, index; // 定位桶數組中的下標位置,index = (n - 1) & hash 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; // 若是鍵的值與鏈表第一個節點相等,則將 node 指向該節點 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 { // 遍歷鏈表,找到待刪除節點 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; }
HashMap中的遍歷也是很是經常使用的API方法,包括;
KeySet
for (String key : map.keySet()) { System.out.print(key + " "); }
EntrySet
for (HashMap.Entry entry : map.entrySet()) { System.out.print(entry + " "); }
從方法上以及平常使用都知道,KeySet是遍歷是無序的,但每次使用不一樣方式遍歷包括keys.iterator()
,它們遍歷的結果是固定的。
那麼從實現的角度來看,這些種遍歷都是從散列表中的鏈表和紅黑樹獲取集合值,那麼他們有一個什麼固定的規律嗎?
測試的場景和前提;
找到18個元素,分別放在不一樣節點(這些數據經過程序計算得來);
代碼測試
@Test public void test_Iterator() { Map<String, String> map = new HashMap<String, String>(64); map.put("24", "Idx:2"); map.put("46", "Idx:2"); map.put("68", "Idx:2"); map.put("29", "Idx:7"); map.put("150", "Idx:12"); map.put("172", "Idx:12"); map.put("194", "Idx:12"); map.put("271", "Idx:12"); System.out.println("排序01:"); for (String key : map.keySet()) { System.out.print(key + " "); } map.put("293", "Idx:12"); map.put("370", "Idx:12"); map.put("392", "Idx:12"); map.put("491", "Idx:12"); map.put("590", "Idx:12"); System.out.println("\n\n排序02:"); for (String key : map.keySet()) { System.out.print(key + " "); } map.remove("293"); map.remove("370"); map.remove("392"); map.remove("491"); map.remove("590"); System.out.println("\n\n排序03:"); for (String key : map.keySet()) { System.out.print(key + " "); } }
這段代碼分別測試了三種場景,以下;
排序01: 24 46 68 29 150 172 194 271 排序02: 24 46 68 29 271 150 172 194 293 370 392 491 590 排序03: 24 46 68 29 172 271 150 194 Process finished with exit code 0
從map.keySet()測試結果能夠看到,以下信息;
moveRootToFront()方法