Java HashMap實現原理2——HashMap詳解
博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起,Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表一文最後,還給出了一個極簡版本的實現。從極簡版出發,理解java.util.HashMap,就容易多了。
最近博主偶遇了幾家公司的技術文章,有一種“相見恨晚“的感覺,繼上次和大家分享騰訊Bugly之後,這裡再和大家分享美團點評的技術網址。也歡迎大家評論裡貢獻自己知道的一些高質量的技術網址,多了之後博主專門整理出來,大家一起學習進步。
Map
鍵值對的儲存在實際程式設計中使用廣泛,Java中實現了HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap等類。它們之間的關係如下圖所示:
現代版的*Map均從Map介面下來,已經不推薦使用的HashTable繼承自Dictionary。
- HashMap根據物件的HashCode存讀資料,多數情況下可以在O(1)的時間內訪問,HashMap允許有null鍵值對,但是null鍵只能有一個。HashMap是執行緒不安全的,如果要在多執行緒情況下使用,可以使用Collections的synchronizedMap方法,或者直接使用ConcurrentHashMap。
- LinkedHashMap是HashMap的子類,記錄了元素放入的順序,使用Iterator遍歷的時候,會按放入的順序依次讀出元素。
- TreeMap會預設按鍵排序,使用Iterator獲取時,會按排好的順序返回。需要注意的是,鍵必須實現了Comparable介面,或者在構造TreeMap的時候傳入自定義的Compartor類。
- HashTable,老版本jdk遺留下來的類,執行緒安全,功能上和HashMap幾乎一樣,併發效能不如引入了分段鎖的ConcurrentHashMap。
以上的Map型別類,要求Key都是物件不可變的,即物件建立後hash值不可變。可採用的方式包括:以String、Integer等型別作為鍵,或者使用物件中不變屬性建立hash值。
HashMap
Map中最常見的是HashMap,它的實現結構就是我們上文提到的散列表(陣列+連結串列)方式,採用鏈地址法處理衝突,連結串列的缺點在於,當衝突很多的時候,連結串列的查詢速度為O(n),JDK8對HashMap的散列表進行了改進,當連結串列儲存元素大於8個時,由紅黑樹結構代替連結串列結構,這樣查詢速度就降到了O(logn)。
先看一個HashMap常見的使用:
HashMap<String,Integer> map = new HashMap();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a"));
先建立HashMap物件,再利用put放入元素,get方法獲取。有了上一篇散列表中對一個小demo的介紹,相信大家對HashMap的結構應該還有大致的印象,忘記的可以在Java HashMap實現原理1——散列表文章最後部分檢視。
1.Node
先看HashMap中定義的Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用來定位陣列索引位置
final K key;
V value;
Node<K,V> next; //連結串列的下一個node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
對比demo中的Node,除了基本的key、 value、next之外,還提供了hash屬性,以及屬性存取的操作介面等,複寫了hashCode()和equals()方法。可見一個可用、完善的類庫這些都是必備條件。結合使用例項,我們先看下HashMap的建構函式。
2.HashMap構造器
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;
threshold = initialCapacity;
init();
}
構造器中初始化了loadFactor、threshold等變數,loadFactor表示負載因子、threshold表示HashMap允許容納的元素個數,threshold=Array.size()*loadFactor,也就是說在當前陣列大小以及負載因子的限定下,HashMap最多能裝的資料個數,當超過這個值時,就得陣列進行擴容resize。系統預設的loadFactor=0.75,值越大,查詢效率降低,空間利用率提高;值越小,查詢效率增大,空間利用率降低,所以一般不建議修改。還有個變數size要與threshold和loadFactor區別開,它表示當前裝載的元素個數。很奇怪,建構函式中沒有初始化陣列,直覺中一般在構造器中完成空間分配的操作,接著往下看。
3.HashMap的put()
put是HashMap的精華部分,裡面用到了hash()、resize()、transfer()等函式,涉及到到不少數學計算中的技巧,我們慢慢欣賞。
put()方法如下:
/*put方法*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/*putVal方法*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// ①:tab為空則建立,put的時候才建立空間,相當於延遲初始化,減少不必要的建立,比在new HashMap()中建立要高明。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// ②:根據hash值計算index
if ((p = tab[i = (n - 1) & hash]) == null)
//對null做處理
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
//③:節點key存在,直接覆蓋value
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) {
if ((e = p.next) == null) {
p.next = newNode(hash, key,value,null);
//連結串列長度大於8轉換為紅黑樹進行處理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已經存在直接覆蓋value
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;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//主要用來記錄HashMap內部結構發生變化的次數,用於迭代的快速失敗(內部結構發生變化指的是結構發生變化,例如put新鍵值對,但是某個key對應的value值被覆蓋不屬於結構變化)
//⑥:超過最大容量 就擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put方法中,先呼叫hash()計算key。我們看下hash的實現
static final int hash(Object key) {
int h;
// 第一步:h = key.hashCode()
// 第二步:h ^ (h >>> 16) 高16位和低16位進行異或運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
有了hash值之後,會根據hash值計算出元素在陣列中的存放位置(n - 1) & hash,乍一看可能會有人有疑問,不是%取模嗎?為什麼用了個按位與。看了下面這張圖片,你可能就明白了:
從生成hashCode開始,到hash()對hashCode進行高16位和低16位的異或操作得到hash值,再利用hash值換算存放的陣列位置。按位與實現了只保留低log(n)的效果,而這log(n)位的取值範圍為[0,n-1],正是我們%取餘的效果,但是對CPU來說&比%省時。
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)函式的流程如下圖所示,程式碼的關鍵位置也有註釋。關於紅黑樹,博主將在下篇文章中專門闡述,此數不再細說。
4.HashMap的resize()
雖然resize是隻是put()的一個呼叫函式,但是resize()很精妙,寫慣了業務邏輯相關的程式碼中後,再看這種util程式碼,很是親切,我們一起來學習一番。
JDK8中的resize()稍微複雜點,我們先看個JDK7的resize()方法壓壓驚。
- JDK7的resize()
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果容量已經達到最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//閾值放大為int的最大整數
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));//將元素全都放入到新的陣列中
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//從原陣列中挨個取出連結串列
while(null != e) {//遍歷連結串列的每個元素
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//重新計算當前元素在新陣列中的位置
e.next = newTable[i];//插入到新陣列對應連結串列的頭部
newTable[i] = e;
e = next;
}
}
}
流程在程式碼中註釋的比較詳細,就不多說了。
- JDK8中的resize()方法
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;//閾值改為最大int整數
return oldTab;
}
// 沒超過最大值,就擴充為原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; //左移1一位,擴大兩倍
}
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);
}
// 計算新的resize上限閾值
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) {
// 把每個bucket都移動到新的buckets中
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 {
// 連結串列優化重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) {//尼瑪,這一塊博主理解了很久。。。oldCap對應於原來的陣列長度,陣列長度總是2的n次方,所以&==0表示倒數第n位(從0開始算)為0 ,意味著擴容到2n之後,該數不需要放入到新增的位置區域,保持原來的陣列位置不變。
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket裡
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket裡
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
對比JDK7和JDK8的resize方法實現,除了JDK7將陣列轉移拆分到單獨的函式transfer裡面之外,JDK7在向新散列表插入元素時,每次都會將新到的元素插入連結串列頭,而JDK8會保持元素在舊陣列的相對位置,即連結串列從前往後依次插入。當然JDK8多了紅黑樹的使用,這個就不多說了。還有個博主糾結了很久的問題,在註釋中已經提過了,就是如何快速找到元素在新散列表中的位置,JDK7採用的是對hash值重新取餘,而JDK8中做了優化,先看圖:
原大小為16的散列表中,位置為5的元素,在新的32大小的散列表中位置為5+16,取餘也可以計算得到,但是JDK8用了更快的方法,e.hash & oldCap==0(oldCap是原散列表中陣列的大小),則表示元素的位置在新的散列表中為j + oldCap,否則不變。
對於hash1和hash2這樣兩個hash值的key,在16大小的散列表中,它們對1111(二進位制)取餘,都得到5,在32大小的散列表中,他們對11111(二進位制)取餘,前者為5,後者為5+16=21。想必看著圖,大家就明白為什麼這麼coding了。
這樣,精華的put部分就講完了,可以看到,巧用位操作,會給你帶來效能上的提升。
5.HashMap的get()
再看下get方法的實現,這個就比較簡單了,程式碼如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/*getEntry()*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
現由key計算得到hash值,再換算到陣列中的位置,取出對應的連結串列,挨個檢查是否和待查詢key,hash值相同且是同一個類的物件或者同一個物件。
總結
本文對HashMap的Node結構、建構函式、put()方法、resize()過程(JDK7和JDK8分別做了分析)、get()方法進行了詳盡的分析,希望可以讓大家對HashMap加深理解。
本文感謝Java 8系列之重新認識HashMap。
很慚愧,做了一點微小的貢獻!
相關推薦
Java HashMap實現原理2——HashMap詳解
博主的前兩篇文章Java HashMap實現原理0——從hashCode,equals說起,Java HashMap實現原理1——散列表已經講述了HashMap設計的知識點,包括:hashCode(),equals(),散列表結構,雜湊函式、衝突解決等,在散列表
hashmap實現原理2
ace 數據 取數 tool 數組存儲 同時 個數 array jsb put和get都首先會調用hashcode方法,去查找相關的key,當有沖突時,再調用equals(這也是為什麽剛開始就重溫hashcode和equals的原因)! HashMap基於hashing原
zabbix實現原理及架構詳解
收集 信息 核心 狀態 start 原理 整體架構 比較 zabbix 想要用好zabbix進行監控,那麽我們首要需要了解下zabbix這個軟件的實現原理及它的架構。建議多閱讀官方文檔。 一、總體上zabbix的整體架構如下圖所示: 重要組件說明: 1)zabbix se
HashMap實現原理詳解
HashMap定義 HashMap實現了Map介面,一種將鍵對映到值得物件。 一個對映不能包含重複的鍵;每個鍵只能對映到一個值上。 HashMap的元素是無序的。要實現有序排列必須實現hashc
1.Java集合-HashMap實現原理及源碼分析
int -1 詳細 鏈接 理解 dac hash函數 順序存儲結構 對象儲存 哈希表(Hash Table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實
【Java】HashMap原始碼分析——常用方法詳解
上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:put()get()**resize()** 首先介紹resize()這個方法,在我看來這是HashMap中一個非常重要的方法,是用來調整HashMap中table的容量的,在很多操作中多需要重新計算容量。原始碼如下: 1
揭祕 HashMap 實現原理(Java 8)
HashMap 作為一種容器型別,無論你是否瞭解過其內部的實現原理,它的大名已經頻頻出現在各種網際網路面試中了。從基本的使用角度來說,它很簡單,但從其內部的實現來看(尤其是 Java 8 的改進以來),它又並非想象中那麼容易。如果你一定要問了解其內部實現與否對於寫程式究竟有多大影響,我不能給出一個確切的答案。
java jdk7 hashMap實現原理
在官方文件中是這樣描述HashMap的: Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and
java HASHMAP 實現原理
1. HashMap概述: HashMap是基於雜湊表的Map介面的非同步實現(Hashtable跟HashMap很像,唯一的區別是Hashtalbe中的方法是執行緒安全的,也就是同步的)。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對
JAVA複習資料-HashMap實現原理
1. HashMap的資料結構 資料結構中有陣列和連結串列來實現對資料的儲存,但這兩者基本上是兩個極端。 陣列 陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為O(1);陣列的特點是:定址容易,插入和刪除困難; 連結串列
HashMap實現原理
一個 ash img 方法 shm 步長 初始 2的n次冪 http HashMap的數據結構是數組+單向鏈表,數組裏面存儲就是鏈表的Head節點,鏈表節點存儲的是我們put進去的key/value。 如果要實現HashMap,主要有三個重要的功能點: 1.初
探索HashMap實現原理及其在jdk8數據結構的改進
重點 his 說了 比較 filter new exist adf 網絡 因為網上已經太多的關於HashMap的相關文章了,為了避免大量重復,又由於網上關於java8的HashMap的相關文章比較少,至少我沒有找到比較詳細的。所以才有了本文。 本文主要的內容: 1.Ha
HashMap實現原理及源碼分析
響應 應用場景 取模運算 圖片 mat 直接 maximum 計算 時間復雜度 哈希表(hash table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也
HashMap實現原理和源碼分析
aci 鍵值對 creat 變化 遍歷數組 沖突的解決 查看 seed 二分 作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>原文:https://www.cnblogs.com/cheng
轉:HashMap實現原理分析(面試問題:兩個hashcode相同 的對象怎麽存入hashmap的)
影響 strong 就會 怎麽 ash 地方 shm nbsp 擔心 原文地址:https://www.cnblogs.com/faunjoe88/p/7992319.html 主要內容: 1)put 疑問:如果兩個key通過hash%Entry[].length得到的
batchnorm原理及程式碼詳解(筆記2)
Batchnorm原理詳解 前言:Batchnorm是深度網路中經常用到的加速神經網路訓練,加速收斂速度及穩定性的演算法,可以說是目前深度網路必不可少的一部分。 本文旨在用通俗易懂的語言,對深度學習的常用演算法–batchnorm的原理及其程式碼實現做一個詳細的解讀。本文主要包括以下幾個
Java程式設計師從笨鳥到菜鳥之(七十二)細談Spring(四)利用註解實現spring基本配置詳解
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
HashMap實現原理(部分原始碼)
JAVA裡面有HashMap、Hashtable、HashSet三種hash集合的實現原始碼,這裡總結下,理解錯誤的地方還望指正! HashMap和Hashtable的區別 1、兩者最主要的區別在於Hashtable是執行緒安全,而HashMap則非執行緒安全。 Hashtabl
HashMap實現原理及原始碼分析(轉載)
作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/> 雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,
HashMap實現原理分析及簡單實現一個HashMap
HashMap實現原理分析及簡單實現一個HashMap 歡迎關注作者部落格 簡書傳送門 轉載@原文地址 HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程式設計師都知道HashMap,都知道哪裡要用HashMap,知道HashMap和