日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JDK源码分析(三)——HashMap 下(基于JDK8)

發布時間:2025/3/15 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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,但是&比%具有更高的效率。計算過程如下圖:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//jdk1.7的源碼,jdk1.8沒有這個方法,但是實現原理一樣的//根據hash值和table數組長度計算鍵值對存儲位置的索引static int indexFor(int h, int length) { return h & (length-1); //第三步 取模運算}

存儲元素

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”

final Node<K,V>[] resize() {//使用oldTab指向原來的hash表,通常方法內都使用局部變量,局部變量在方法棧上,而對象的成員在堆上//方法棧的訪問比堆更高效//記錄擴容前table數組,閾值,長度Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {//原來的容量已經很大了,超過了MAXIMUM_CAPACITY無法再調整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}//oldCap < 0,oldThr > 0,table空間尚未分配,初始化分配空間//舊閥值大于0,則將新容量直接等于就閥值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//閥值等于0,oldCap也等于0(集合未進行初始化)else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;//threshhold=capacity*load_factornewThr = (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;//原來的表中有內容,表明這是一次擴容,需要將Entry散列到新的位置if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {//遍歷所有binNode<K,V> e;if ((e = oldTab[j]) != null) {//舊桶中元素置為null,方便GColdTab[j] = null;//該桶中只有一個節點,直接散列到新的位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)//該桶中是一顆紅黑樹,通過紅黑樹的split方法處理//待會再看split方法((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 與oldCap按位相與,判斷結果是一還是零if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//原索引if (loTail != null) {loTail.next = null;newTab[j] = loHead;}//原索引+oldCapif (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

刪除元素

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)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。