前一篇博文 JDK容器學習之HashMap (一) : 底層存儲結構分析 分析了HashMap的底層存儲數據結構java
經過
put(k,v)
方法的分析,說明了爲何Map底層用數組進行存儲,爲何Node
內部有一個next
節點,這篇則將集中在讀寫方法的具體實現上node
本片博文將關注的重點:數組
value
的實現邏輯table
數組如何自動擴容
get(key)
做爲map最經常使用的方法之一,根據key獲取映射表中的value,一般時間複雜度爲o(1)
數據結構
在分析以前,有必要再把HashMap
的數據結構撈出來看一下ide
根據上面的結構,若是讓咱們本身來實現這個功能,對應的邏輯應該以下:學習
table
數組中的位置next
節點的key,直到找到爲止jdk實現以下測試
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 內部邏輯以下 // table 數組已經初始化(即非null,長度大於0) // 數組中根據key查到的Node對象非空 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; }
上面的邏輯算是比較清晰,再簡單的劃一下重點this
hash(key) & (table.length - 1)
node.hash == hash(key) && (node.key == key || (key!=null && key.equals(node.key))
and == or equals
上面的邏輯中,當出現hash碰撞時,會判斷數組中的Node
對象是否爲 TreeNode
,若是是則調用 TreeNode.getTreeNode(hash,key)
方法.net
那麼這個TreeNode有什麼特殊的地方呢?code
TreeNode
分析
TreeNode
依然是HashMap
的內部類, 不一樣於Node的是,它繼承自LinkedHashMap.Entry
,相比較與Node
對象而言,多了兩個屬性before, after
TreeNode對象中,包含的數據以下(將父類中的字段都集中在下面了)
// Node 中定義的屬性 final int hash; final K key; V value; Node<K,V> next; // --------------- // LinkedHashMap.Entry 中的屬性 Entry<K,V> before, after; // --------------- // TreeNode 中定義的屬性 TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red;
方法比較多,實現也很多,可是看看方法名以及註釋,很容易猜到這是個什麼東西了
紅黑樹
具體方法實現身略(對紅黑樹實現有興趣的,就能夠到這裏來膜拜教科書的實現方式)
普通的Node就是一個單向鏈表,所以HashMap的結構就是上面哪一種
TreeNode是一顆紅黑樹的結構,因此對上面的圖走一下簡單的改造,將單向鏈表改爲紅黑樹便可
博文 JDK容器學習之HashMap (一) : 底層存儲結構分析 對於添加kv對的邏輯進行了說明,所以這裏將主要集中在數組的擴容上
擴容的條件: 默認擴容加載因子爲(0.75),臨界點在當HashMap中元素的數量等於table數組長度*加載因子,長度擴爲原來的2倍
數組擴容方法, 實現比較複雜,先擼一把代碼,並加上必要註釋
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) // 新的數組長度設置爲原數組長度的兩倍 // 新的閥值爲舊閥值的兩倍, newThr = oldThr << 1; // double threshold } else if (oldThr > 0) { // initial capacity was placed in threshold newCap = oldThr; } else { // 首次初始化 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; 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) { // 若Node節點沒有出現hash碰撞,則直接塞入新的數組 newTab[e.hash & (newCap - 1)] = e; } else if (e instanceof TreeNode) { // 對於出現hash碰撞,且紅黑樹結構時,須要從新分配 ((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; if ((e.hash & oldCap) == 0) { // 新的位置相比原來的新增了 oldCap 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; }
上面的邏輯主要劃分爲兩塊
說明
這個擴容的邏輯仍是比較有意思的,最後面給一個測試case,來看一下擴容先後的數據位置
刪除的邏輯和上面的大體相似,顯示肯定節點,而後從整個數據結構中移除引用
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; // 刪除的前置條件: // 1. 數組已經初始化 // 2. key對應的Node節點存在 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節點即爲目標 node = p; } else if ((e = p.next) != null) { // hash碰撞,目標可能在鏈表or紅黑樹中 // 便利鏈表or紅黑樹,肯定目標 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)))) { // 找到目標節點,直接從數組or紅黑樹or鏈表中移除 // 不改變Node節點的內容 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; }
上面的幾個經常使用方法的邏輯大體相同,核心都是在如何找到目標Node節點,其中比較有意思的一點是數組的擴容,舊元素的遷移邏輯,下面寫個測試demo來演示一下
首先定義一個Deom對象,覆蓋hashCode
方法,確保第一次從新分配數組時,正好須要遷移
public static class Demo { public int num; public Demo(int num) { this.num = num; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Demo demo = (Demo) o; return num == demo.num; } @Override public int hashCode() { return num % 3 + 16; } } @Test public void testMapResize() { Map<Demo, Integer> map = new HashMap<>(); for(int i = 1; i < 12; i++) { map.put(new Demo(i), i); } // 下面這一行執行,並不會觸發resize方法 map.put(new Demo(12), 12); // 執行下面這一行,會觸發HashMap的resize方法 // 由於 hashCode值 & 16 == 1,因此新的位置會是原來的位置+16 map.put(new Demo(13), 13); }
實際演示示意圖
hash & (len - 1) === hash % len
key1 == key2 or key1.quals(key2)
loadFactor
; loadFactory
通常爲 0.75(hash % 原長度 == 0)
(hash % 原長度 == 1)
相關博文
掃一掃二維碼,關注 小灰灰blog