JDK源码分析(三)——HashMap 下(基于JDK8)
目錄
- 概述
- 內部字段及構造方法
- 哈希值與索引計算
- 存儲元素
- 擴容
- 刪除元素
- 查找元素
- 總結
概述
??在上文我們基于JDK7分析了HashMap的實現源碼,介紹了HashMap的加載因子loadFactor、閾值threshold概念以及增刪元素的機制。JDK8在JDK7的基礎上對HashMap的實現進行了進一步的優化,最主要的改變就是新增了紅黑樹作為底層數據結構。
HashMap數據結構
??首先我們回憶一下JDK7中HashMap的實現,HashMap是以數組和單鏈表構成,當出現哈希沖突時,沖突的元素在桶中依次形成單鏈表,數據結構如下:
JDK7中哈希沖突時使用鏈表存儲沖突元素,當出現大量哈希沖突元素,那么在單鏈表查找一個元素的復雜度為O(N),為了優化出現大量哈希沖突元素的查找問題,JDK8中規定:當單鏈表存儲元素個數超過閾值TREEIFY_THRESHOLD(8)時,將單鏈表轉換為紅黑樹,紅黑樹查找元素復雜度為O(logN),提高了查找效率,JDK8中HashMap的存儲結構:
內部字段及構造方法
Node類
??使用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;}}TreeNode類
??TreeNode是構成紅黑樹的基本元素。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red;//構造一個樹結點TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}}內部字段
//數組初始容量,為16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//數組最大容量static final int MAXIMUM_CAPACITY = 1 << 30;//默認加載因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//單鏈表轉化為紅黑樹的閾值static final int TREEIFY_THRESHOLD = 8;/*** 主要用于resize()擴容過程中, 當對原來的紅黑樹根據hash值拆分成兩條鏈表后,* 如果拆分后的鏈表長度 <=UNTREEIFY_THRESHOLD, 那么就采用鏈表形式管理hash值沖突;* 否則, 采用紅黑樹管理hash值沖突.*/static final int UNTREEIFY_THRESHOLD = 6;/*** 當集合中的容量大于這個值時,表中的桶才能進行樹化 ,否則桶內元素太多時會擴容,* 而不是樹形化 為了避免進行擴容、樹形化選擇的沖突,這個值不能小于 4 * TREEIFY_THRESHOLD*/static final int MIN_TREEIFY_CAPACITY = 64;//第一次使用是初始化,數組長度總是2的冪次transient Node<K,V>[] table;transient int size;int threshold;final float loadFactor;構造方法
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;//這里只是初始化,最終賦值在resize方法里this.threshold = tableSizeFor(initialCapacity);}哈希值與索引計算
hash(Object key)
??在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
??這個方法非常巧妙,它通過h & (table.length -1)來得到該對象的保存位,而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價于對length取模,也就是h%length,但是&比%具有更高的效率。計算過程如下圖:
存儲元素
put(K key, V value)
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** Implements Map.put and related methods* @param hash hash for key* @param key the key 鍵* @param value the value to put 值* @param onlyIfAbsent if true, don't change existing value 表示不要更改現有值* @param evict if false, the table is in creation mode. false表示table處于創建模式* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//使用局部變量tab而不是類成員,方法棧上訪問更快Node<K,V>[] tab; Node<K,V> p; int n, i;//如果table為null或者長度為0,則進行初始化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 {//else分支表示散列到的桶中元素不為空Node<K,V> e; K k;//桶中鏈表的根節點的key就是要插入的鍵值對的keyif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//如果是紅黑樹,插入到紅黑樹中else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//插入到單鏈表中else {//遍歷鏈表,并統計桶中鍵值對元素數量for (int binCount = 0; ; ++binCount) {//該Key在鏈表中不存在,插入末尾 此時e為nullif ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//單鏈表元素個數超過TREEIFY_THRESHOLD,樹化if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;//注意從這里break出來的e為null}//該Key已經在鏈表中if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))//注意從這里break出來的e不為nullbreak;p = e;}}// e != null,說明該Key已經在存在于HashMap中,在這個桶中if (e != null) { // existing mapping for keyV oldValue = e.value;//根據onlyIfAbsent和oldif (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//超過閾值,就進行擴容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}擴容
resize( )
??resize方法相對比較復雜,但是有比較巧妙,因為要考慮數據結構的不同怎么把元素從老的table中放到擴容后的table中。主要的思路也就是兩步:先根據老的table的長度決定擴容后table的長度,以及新的閾值threshold;在桶中數據不為空的情況下,把桶中的數據遷移到新的table數組中,這里就要考慮在桶中只有一個元素(沒有發生哈希沖突)、桶中元素以單鏈表形式存儲(發生哈希沖突但是不超過8個)、桶中元素以紅黑樹形式存儲(哈希沖突元素個數超過8個)。只有一個元素,直接根據哈希值和新的table數組長度計算出新的索引,紅黑樹調用split方法,這里我們重點分析一下怎么把桶中的單鏈表遷移到新桶中,從而體會到JDK的巧妙設計。
??resize的擴容策略是每次擴容2倍(newThr = oldThr << 1),為了把單鏈表元素遷移到新的桶中,并不是向JDK7那樣直接根據哈希值散列得到新的索引值,經過觀測可以發現,我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。
元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”
刪除元素
remove(Object key)
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;//tab不為空,p指向根據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;//第一個節點就命中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;//移除的節點不是單鏈表首節點elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;}查找元素
get(Object key)
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 &&(first = tab[(n - 1) & hash]) != null) {//hash值對應的桶中第一個元素//第一個元素符合查找條件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;}總結
??JDK8使用了紅黑樹優化了HashMap的性能,即使發生了大量的哈希碰撞也能夠以O(logN)查找到元素,不會影響到服務器的性能。
轉載于:https://www.cnblogs.com/rain4j/p/9536570.html
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的JDK源码分析(三)——HashMap 下(基于JDK8)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Modbus通信协议 【 初识 Modb
- 下一篇: 内置函数补充 之 反射