Java8 HashMap詳解
Java8 HashMap
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。
根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決於連結串列的長度,為 O(n)。
為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。
來一張圖簡單示意一下吧:
注意,上圖是示意圖,主要是描述結構,不會達到這個狀態的,因為這麼多資料的時候早就擴容了。
下面,我們還是用程式碼來介紹吧,個人感覺,Java8 的原始碼可讀性要差一些,不過精簡一些。
Java7 中使用 Entry 來代表每個 HashMap 中的資料節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node
只能用於連結串列的情況,紅黑樹的情況需要使用 TreeNode
。
我們根據陣列元素中,第一個節點資料型別是 Node 還是 TreeNode 來判斷該位置下是連結串列還是紅黑樹的。
put 過程分析
public V put(K key, V value) {
return putVal(hash(key), key, value , false, true);
}
// 第三個引數 onlyIfAbsent 如果是 true,那麼只有在不存在該 key 時才會進行 put 操作
// 第四個引數 evict 我們這裡不關心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化陣列長度
// 第一次 resize 和後續的擴容有些不一樣,因為這次是陣列從 null 初始化到預設的 16 或自定義的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具體的陣列下標,如果此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 陣列該位置有資料
Node<K,V> e; K k;
// 首先,判斷該位置的第一個資料和我們要插入的資料,key 是不是"相等",如果是,取出這個節點
if (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) {
// 插入到連結串列的最後面(Java7 是插入到連結串列的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 為 8,所以,如果新插入的值是連結串列中的第 9 個
// 會觸發下面的 treeifyBin,也就是將連結串列轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在該連結串列中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此時 break,那麼 e 為連結串列中[與要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 說明存在舊值的key與要插入的key"相等"
// 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然後返回舊值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
和 Java7 稍微有點不一樣的地方就是,Java7 是先擴容後插入新值的,Java8 先插值再擴容,不過這個不重要。
陣列擴容
resize()
方法用於初始化陣列或陣列擴容,每次擴容後,容量為原來的 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) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候
newCap = oldThr;
else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候
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;
// 用新的陣列大小初始化新的陣列
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 如果是初始化陣列,到這裡就結束了,返回 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)
newTab[e.hash & (newCap - 1)] = e;
// 如果是紅黑樹,具體我們就不展開了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 這塊是處理連結串列的情況,
// 需要將此連結串列拆成兩個連結串列,放到新的陣列中,並且保留原來的先後順序
// loHead、loTail 對應一條連結串列,hiHead、hiTail 對應另一條連結串列,程式碼還是比較簡單的
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;
// 第二條連結串列的新的位置是 j + oldCap,這個很好理解
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get 過程分析
相對於 put 來說,get 真的太簡單了。
- 計算 key 的 hash 值,根據 hash 值找到對應陣列下標: hash & (length-1)
- 判斷陣列該位置處的元素是否剛好就是我們要找的,如果不是,走第三步
- 判斷該元素型別是否是 TreeNode,如果是,用紅黑樹的方法取資料,如果不是,走第四步
- 遍歷連結串列,直到找到相等(==或equals)的 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) {
// 判斷第一個節點是不是就是需要的
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;
}
相關推薦
Java8 HashMap詳解
Java8 HashMap Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。 根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具
HashMap詳解
== nan lba illegal 需要 我們 bar toolbar 它的 HashMap也是我們使用非常多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,
java程式設計思想讀書筆記三(HashMap詳解)
Map Map介面規定了一系列的操作,作為一個總規範它所定義的方法也是最基礎,最通用的。 AbstractMap AbstractMap是HashMap、TreeMap,、ConcurrentHashMap 等類的父類。當我們巨集觀去理解Map時會發現,其實Map就是一
HashMap 詳解五
紅黑樹性質 紅黑樹是平衡二叉樹的一種, 但是它的平衡因子是可以大於 1 紅黑樹的節點要麼是紅色, 要麼是黑色, 這裡的紅黑色只是用來區分的一種方式, 為了定義規則 根節點一定是黑色 葉子節點也是黑色, 實際上葉子節點都是由 NULL 組成 紅色節點的子節點是黑色 根節點到葉
HashMap 詳解六
連結串列轉樹結構 根據詳解四, 當連結串列長度大於 8 時, 為了更高效的查詢, 需要轉成紅黑樹結構, 使用的方法是 treeifyBin. 過程是先把連結串列結構調整為雙向連結串列結構, 再把雙向連結串列結構調整為紅黑樹結構. /** * tab: 陣列 * hash: 新節點 key 的雜
Java:hashMap詳解
原 Java集合:HashMap詳解(JDK 1.8) 置頂 2018年01月07日 18:00:41 JoonWhee
HashMap詳解( JDK8 之前與之後對比)
HashMap簡介 HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。 HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。 HashMap 的實現不是同步的,這意味著它不是執行緒安全的。它的k
hashMap詳解與例項
在Java集合類中最常用的除了ArrayList外,就是HashMap了。本文儘自己所能,儘量詳細的解釋HashMap的原始碼。一山還有一山高,有不足之處請之處,定感謝指定並及時修正。 在看HashMap原始碼之前先複習一下資料結構。 Ja
java8 Stream詳解
Stream組成 在傳統Java程式設計,或者說是類C語言程式設計中,我們如何操作一個數組資料呢?或者更泛化的講,我們如何操作一個“集合”(Collection)資料呢?在Java中我們利用java.util包裡的各種資料結構封裝,來很好的表示了陣列(Array)、
java中HashMap詳解
上面方法的程式碼很簡單,但其中包含了一個非常優雅的設計:系統總是將新新增的 Entry 物件放入 table 陣列的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 物件,那新新增的 Entry 物件指向原有的 Entry 物件(產生一個 Entry 鏈),如果
面試題--HashMap詳解
先上hashCode和equals原始碼: /** JNI,呼叫底層其它語言實現 */ public native int hashCode(); /** 默認同==,直接比較物件 */ public boolean equals(Object
Java集合(四)HashMap詳解
HashMap簡介 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> public class HashMap<K,V> ex
Java HashMap實現原理2——HashMap詳解
博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起,Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表
Java8 ConcurrentHashMap詳解
Java8 ConcurrentHashMap Java7 中實現的 ConcurrentHashMap 說實話還是比較複雜的,Java8 對 ConcurrentHashMap 進行了比較大的改動。建議讀者可以參考 Java8 中 HashMap 相對於
Java8 Optional詳解
在Java8中新增了一個Optional類,官方描述是該類是一個容器物件,其中可能包含一個空或非空的值。如果存在一個值,isPresent()將返回true,get()將返回該值。 錯誤使用姿勢 簡單的根據描述,我們認為Optional可以幫我們解決NP
java8 lambda詳解
一)輸入引數在Lambda表示式中,輸入引數是Lambda運算子的左邊部分。它包含引數的數量可以為0、1或者多個。只有當輸入引數為1時,Lambda表示式左邊的一對小括弧才可以省略。輸入引數的數量大於或者等於2時,Lambda表示式左邊的一對小括弧中的多個引數質檢使用逗號(,
Java8學習筆記(五)--Stream API詳解[轉]
有效 編程效率 實時處理 phaser 綜合 files -- bin 並發模式 為什麽要使用StreamStream 作為 Java 8 的一大亮點,它與 java.io 包裏的 InputStream 和 OutputStream 是完全不同的概念。它也不同於 StAX
HashTable和HashMap的區別詳解
body 線程安全 serializa javadoc cloneabl 允許 哈希 安全性 rac HashMap是基於哈希表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決沖突問題,容量不足(超過了閥值)時,同樣會自動增長。 HashMap
HashMap重點詳解
映射 != ash lstat 放置 運算 畫圖 blog while Map即映射表一般稱為散列表。開發中常用到這種數據結構,Java中HashMap和ConcurrentHashMap被用到的頻率較高,本文重點說下HashMap的實現原理以及設計思路。
【Java】HashMap源碼分析——常用方法詳解
fir 設置 直接 dfa 構造方法 change mage null 這也 上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:put()get()**resize()** 首先介紹resize()這個方法,在我看來這是HashMap中一個