經常使用的數據結構在JDK中,數據的一種實現是ArrayList,鏈表的一種實現是LinkedList,基於數組和鏈表的特性不難發現,數組根據索引檢索很快,但寫入和刪除時會發生copy消耗性能,鏈表寫入刪除很快,但檢索就須要遍歷。那麼是否有一種數據結構寫入也不太慢,檢索也不太慢呢?Hash表就是這麼一種結構,其在JDK中的實現之一,就是今天要分析的HashMap。html
HashMap與ArrayList同樣,是咱們在平常編程中常用的,因此選擇對它進行一次源碼分析。java
JDK1.8中的HashMap的源碼,算上註釋大約有2400行左右,分析HashMap與分析ArrayList和LinkedList不一樣,須要先從HashMap的規則進行分析:node
在瞭解了JDK是如何對HashMap的關鍵點進行設計後,再來閱讀代碼,就會順利不少了。算法
前提:重中之重,HashMap中散列表的長度,永遠是2^n!!!編程
簡單歸納的話,HashMap是經過對元素的Key進行hash算法後獲得hash值,而後將hash值與數組(也就是hash表)長度進行計算,得到一個索引,這個索引就是元素在數組中的位置,接着將元素保存到該位置(暫時不考慮hash衝突)。數組
HashMap中的hash算法與定位,可謂是整個HashMap實現的精髓之一了,下面分析代碼:安全
// hash算法 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
// 定位元素 tab[i = (n - 1) & hash]
這幾行代碼便是HashMap的hash算法和定位方法,網上對這幾行代碼的講解文章有不少,我貼出幾個鏈接並進行簡單說明和總結:數據結構
https://www.cnblogs.com/wang-meng/p/9b6c35c4b2ef7e5b398db9211733292d.html app
https://blog.csdn.net/a314774167/article/details/100110216 less
先說hashcode算法步驟:
這段代碼的重點在於步驟2和步驟3,爲何要作hashcode ^ (hashcode >>> 16),上面鏈接中的文章描述的很清楚了。
============================================================================
============================================================================
將hashcode的高位16位於低位16位進行異或,可使hashcode分散的更平均,減小hash碰撞。
在說說定位方法,定位方式其實並無什麼神祕的地方,思路就是用hashcode與數組長度取模。那爲何直接用 hashcode % length,而是用 length - 1 & hashcode進行定位呢?
這裏實際上是有一個性能考慮的,總所周知,在計算機計算時,位操做要比取模操做快,若一次比較可能感受不出來,可是當數組進行擴容時,會重新的數組中的元素進行位置分配,這時若是元素數量多,那麼位操做的高效率就體現出來了。
若要用位操做代替取模,其關鍵點與精髓就在於數組長度永遠是2^n!
基於二進制的特性,以int爲例,2^n - 1 的有效爲永遠是1。舉幾個形象的例子:
16的2進制=10000;15的2進制=1111;
32的2進制=100000;31的2進制=11111;
64的2進制=1000000;63的2進制=111111;
若是用length & hashcode,那麼會被0給屏蔽掉,但用length - 1 & hashcode就不存在這種問題,由於有效爲都是1,因此&的效果更好。
因此,要作到用位運算代替取模來提高效率,須要讓數組的長度必須是2^n。
當兩個Key的hashCode通過計算後,依然相同,這種狀況就被稱爲hash衝突。經常使用的解決hash衝突的兩個辦法是鏈表發和開發尋址法。HashMap使用鏈表法來解決Hash衝突。
作法是將多個衝突的元素,組成一個鏈表。在通過定位後,順序查找鏈表找到真正要找的元素,查找鏈表的時間複查度爲O(n),n等於鏈表長度。
因爲查詢鏈表的時間複雜度爲O(n),當數組某個索引位置的鏈表過長時,查詢效率依舊不高。因此在JDK1.8中,當鏈表長度打到一個閾值時,Hashmap會將鏈表轉換爲紅黑樹。
// 鏈表轉換爲紅黑樹的鏈表長度閾值 static final int TREEIFY_THRESHOLD = 8; // 紅黑樹轉換爲鏈表的鏈表長度閾值 static final int UNTREEIFY_THRESHOLD = 6; // 鏈表轉換爲紅黑樹的數組長度閾值 static final int MIN_TREEIFY_CAPACITY = 64;
鏈表轉換爲紅黑樹,或將紅黑樹轉換爲鏈表的閾值有以上三個。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);
當鏈表中的元素大於TREEIFY_THRESHOLD時候,會嘗試調用treeifyBin將鏈表轉換爲紅黑樹。爲何說嘗試呢?
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
treeifyBin方法中會判斷,若是數組的長度小於MIN_TREEIFY_CAPACITY時,不進行紅黑樹轉換,而是對數組進行一個resize。只有當長度不小於MIN_TREEIFY_CAPACITY時,纔對鏈表作紅黑樹轉換。
當鏈表中的元素小於UNTREEIFY_THRESHOLD時,將紅黑樹轉換爲鏈表。
if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map);
爲何將這幾個閾值設定爲8,6,64,個人理解是,這些是經驗值,是對時間複雜度和空間複雜度的一個平衡。
先說64,當鏈表長度>=8時,會嘗試轉換紅黑樹,但要求數組長度必須<64。HashMap源碼註釋中說道,一個紅黑樹的內存佔用是一個鏈表的2倍。因此當<64這個經驗閾值時,只對數組作resize,resize的同時會rehash。這是平衡時間和空間兩個複雜度的設計。
再說8和6,在HashMap源碼中,有一段註釋是Implementation notes.
* Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
大體意思是,在一個良好的hash算法下使用HashMap,發生鏈表轉紅黑樹的機率是很小的,這個機率的依據是泊松分佈。
註釋中作了一些數聽說明,依據公式,默認使用擴容閾值0.75時,出現hash衝突8次的機率是0.00000006,機率很小。因此這也是平衡時間和空間兩個複雜度的設計。
至於將紅黑樹轉換爲鏈表選擇了6而不是8,是爲了不頻換轉換帶來的耗損。
瞭解了hash定位和hash衝突後,會清楚HashMap內部維護了一個數組來保存數據,數組的長度是2^n,當hash衝突時經過鏈表法解決問題,當鏈表過長時會轉換爲紅黑樹。
那麼當這個數組(散列表)容量不足時,如何擴容是一個關鍵點,下面來看看HashMap是如何對數組進行擴容的。
// 數組的默認長度 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 數組的最大長度 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子,意思是當數組中的容量達到75%時,會擴容 static final float DEFAULT_LOAD_FACTOR = 0.75f;
上面是擴容的幾個關鍵閾值,具體擴容代碼以下:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // ======================================================= // 計算相關閾值 // 記錄當前數組的長度和擴容閾值,建立新數組的長度和擴容閾值 // 因爲HashMap是懶加載,也就是說new HashMap()的時候數組還爲建立 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 設置閾值,若是舊數組容量>0,說明已經建立過數組 // 那麼嘗試設置新數組長度和新擴容閾值 if (oldCap > 0) { // 若是,舊數組容量>=(1<<30),擴容閾值設置爲Integer.MAX_VALUE // 並返回舊數組,不進行數組擴容了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 不然,新數組長度=舊數組長度的2倍 // 並判斷,若是新數組長度 < (1<<30) 而且 舊數組長度 >= 16 // 則新的擴容閾值等於舊擴容閾值的兩倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 若是舊擴容閾值>0,說明new HashMap的時候設置了數組長度,可是還未初始化數組 // 那麼就將設置的舊數組長度賦值給新數組長度 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 不然,說明舊數組長度=0而且舊擴容閾值=0,也就是默認的new HashMap // 那麼就用默認的比那輛來設置新數組長度和擴容閾值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 若是新擴容閾值通過上面的計算後仍是0 // 那麼根據新的數組長度 * 填充因子,設置新的擴容閾值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 將計算好的新擴容閾值設置給threshold 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) // 從新進行hashkey計算,並寫入數組 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // hash衝突,節點是樹,則對紅黑樹進行操做 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // hash衝突,節點是鏈表,則對鏈表進行操做 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) { 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; }
代碼比較沉長,主要分爲計算閾值和操做數據結構兩個部分,源碼中都有體現,總結一下幾個擴容的關鍵點:
HashMap的經常使用方法基本上是對數組、鏈表和紅黑樹的操做,數組和鏈表在ArrayList與LinkedList中都有基本作法,因此就不過多記錄了。
// 鏈表對象數據結構 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; } }
// 紅黑樹數據結構 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 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; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } /** * Returns root of tree containing this node. */ final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } }
// 默認構造函數 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } // 可初始化容量 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 可初始化容量和填充因子 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; // 將傳遞進來的初始化容量修整爲n^2 this.threshold = tableSizeFor(initialCapacity); } // 可從其餘繼承Map接口的對象初始化HashMap public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } // 將傳遞進來的初始化容量修整爲n^2 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
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; // first = tab[(n - 1) & hash]是定位元素 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 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; if ((tab = table) == null || (n = tab.length) == 0) // 數組爲null,調用resize建立數組 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) // hash不衝突,put元素 tab[i] = newNode(hash, key, value, null); else { // hash衝突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 若是put了重複元素 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) // 超過閾值,嘗試鏈表轉紅黑樹 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; // 判斷是否容許覆蓋,而且value是否爲空 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 回調容許LinkedHashMap後置操做 return oldValue; } } ++modCount; if (++size > threshold) // 添加元素後判斷是否須要擴容 resize(); afterNodeInsertion(evict); // 回調以容許LinkedHashMap後置操做 return null; }
public boolean remove(Object key, Object value) { return removeNode(hash(key), key, value, true, true) != null; } 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 { // 操做鏈表 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; }
Get,Put,Remove方法都是基本上都是對數組、鏈表或紅黑樹的操做,Put時候會觸發擴容,Remove時會先定位到元素,而後進行刪除。
HashMap還有不少其餘方法,例如:putAll,putIfAbsent,size,isEmpty,containsKey,containsValue,clear,merge等等……
基本上都是對內部數據結構的操做,再次就不過多記錄了,能夠直接閱讀源碼。
HashMap取ArrayList與LinkedList的有點,並綜合了數組、鏈表和紅黑樹,提供了基於KeyValue的Hash表數據結構。
HashMap對hash算法與定位、hash衝突和hash表擴容方面的設計很巧妙,對時間複雜度和空間複雜度作到了極大的權衡。hash定位使用長度與hashcode進行計算得出位置,hash衝突使用鏈表法解決,擴容時會rehash。
HashMap是很是值得閱讀的JDK源碼,因爲項目中基本都會使用,因此結合場景去閱讀會很是深入。
從實戰的角度去學習hash算法,以及hash表數據結構,也接觸了紅黑樹的概念,以及鞏固了數組與鏈表的操做方法。
以上,是對HashMap源碼分析的記錄。