HashMap的實現原理(看這篇就夠了)
HashMap 是一線資深 java工程師必須要精通的集合容器,它的重要性幾乎等同於Volatile在併發程式設計的重要性(可見性與有序性)。本篇通過圖文原始碼詳解,深度剖析 HashMap 的重要核心知識,易看易學易懂。建議收藏,多學一點總是好的,萬一面試被問到了呢。
我是Mike,10餘年BAT一線大廠架構技術傾囊相授。
本篇重點:
1.HashMap的資料結構
2.HashMap核心成員
3.HashMapd的Node陣列
4.HashMap的資料儲存
5.HashMap的雜湊函式
6.雜湊衝突:鏈式雜湊表
7.HashMap的get方法:雜湊函式
8.HashMap的put方法
9.為什麼槽位數必須使用2^n?
HashMap的資料結構
首先我們從資料結構的角度來看:HashMap是:陣列+連結串列+紅黑樹(JDK1.8增加了紅黑樹部分)的資料結構,如下所示:
這裡需要搞明白兩個問題:
-
資料底層具體儲存的是什麼?
-
這樣的儲存方式有什麼優點呢?
1.核心成員
預設初始容量(陣列預設大小):16,2的整數次方static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; 預設負載因子static final float DEFAULT_LOAD_FACTOR = 0.75f; 裝載因子用來衡量HashMap滿的程度,表示當map集合中儲存的資料達到當前陣列大小的75%則需要進行擴容 連結串列轉紅黑樹邊界static final int TREEIFY_THRESHOLD = 8; 紅黑樹轉離連結串列邊界static final int UNTREEIFY_THRESHOLD = 6; 雜湊桶陣列transient Node<K,V>[] table; 實際儲存的元素個數transient int size; 當map裡面的資料大於這個threshold就會進行擴容int threshold 閾值 = table.length * loadFactor
2.Node陣列
從原始碼可知,HashMap類中有一個非常重要的欄位,就是 Node[] table,即雜湊桶陣列,明顯它是一個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) { 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; } }
Node是HashMap的一個內部類,實現了Map.Entry介面,本質是就是一個對映(鍵值對)。
HashMap的資料儲存
1.雜湊表來儲存
HashMap採用雜湊表來儲存資料。
雜湊表(Hash table,也叫雜湊表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構,只要輸入待查詢的值即key,即可查詢到其對應的值。
雜湊表其實就是陣列的一種擴充套件,由陣列演化而來。可以說,如果沒有陣列,就沒有雜湊表。
2.雜湊函式
雜湊表中元素是由雜湊函式確定的,將資料元素的關鍵字Key作為自變數,通過一定的函式關係(稱為雜湊函式),計算出的值,即為該元素的儲存地址。
表示為:Addr = H(key),如下圖所示:
雜湊表中雜湊函式的設計是相當重要的,這也是建雜湊表過程中的關鍵問題之一。
3.核心問題
建立一個雜湊表之前需要解決兩個主要問題:
1)構造一個合適的雜湊函式,均勻性 H(key)的值均勻分佈在雜湊表中
2)衝突的處理
衝突:在雜湊表中,不同的關鍵字值對應到同一個儲存位置的現象。
4.雜湊衝突:鏈式雜湊表
雜湊表為解決衝突,可以採用地址法和鏈地址法等來解決問題,Java中HashMap採用了鏈地址法。
鏈地址法,簡單來說,就是陣列加連結串列的結合,如下圖所示:
HashMap的雜湊函式
/** * 重新計算雜湊值 */static final int hash(Object key) { int h; // h = key.hashCode() 為第一步 取hashCode值 // h ^ (h >>> 16) 為第二步 高位參與運算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
//計算陣列槽位
(n - 1) & hash
對key進行了hashCode運算,得到一個32位的int值h,然後用h 異或 h>>>16位。在JDK1.8的實現中,優化了高位運算的演算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16)。
這樣做的好處是,可以將hashcode高位和低位的值進行混合做異或運算,而且混合後,低位的資訊中加入了高位的資訊,這樣高位的資訊被變相的保留了下來。
等於說計算下標時把hash的高16位也參與進來了,摻雜的元素多了,那麼生成的hash值的隨機性會增大,減少了hash碰撞。
備註:
-
^異或:不同為1,相同為0
-
>>> :無符號右移:右邊補0
-
&運算:兩位同時為“1”,結果才為“1,否則為0
h & (table.length -1)來得到該物件的儲存位,而HashMap底層陣列的長度總是2的n次方。
為什麼槽位數必須使用2^n?
1.為了讓雜湊後的結果更加均勻
假如槽位數不是16,而是17,則槽位計算公式變成:(17 – 1) & hash
從上文可以看出,計算結果將會大大趨同,hashcode參加&運算後被更多位的0遮蔽,計算結果只剩下兩種0和16,這對於hashmap來說是一種災難。2.等價於length取模
當length總是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。
位運算的運算效率高於算術運算,原因是算術運算還是會被轉化為位運算。
最終目的還是為了讓雜湊後的結果更均勻的分部,減少雜湊碰撞,提升hashmap的執行效率。
分析HashMap的put方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 當前物件的陣列是null 或者陣列長度時0時,則需要初始化陣列 if ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length; } // 使用hash與陣列長度減一的值進行異或得到分散的陣列下標,預示著按照計算現在的 // key會存放到這個位置上,如果這個位置上沒有值,那麼直接新建k-v節點存放 // 其中長度n是一個2的冪次數 if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null); } // 如果走到else這一步,說明key索引到的陣列位置上已經存在內容,即出現了碰撞 // 這個時候需要更為複雜處理碰撞的方式來處理,如連結串列和樹 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) { // 其中this表示當前HashMap, tab為map中的陣列 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); // TREEIFY_THRESHOLD = 8 // 從0開始的,如果到了7則說明滿8了,這個時候就需要轉 // 重新確定是否是擴容還是轉用紅黑樹了 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 找到了碰撞節點中,key完全相等的節點,則用新節點替換老節點 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 此時的e是儲存的被碰撞的那個節點,即老節點 if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent是方法的呼叫引數,表示是否替換已存在的值, // 在預設的put方法中這個值是false,所以這裡會用新值替換舊值 if (!onlyIfAbsent || oldValue == null) e.value = value; // Callbacks to allow LinkedHashMap post-actions afterNodeAccess(e); return oldValue; } } // map變更性操作計數器 // 比如map結構化的變更像內容增減或者rehash,這將直接導致外部map的併發 // 迭代引起fail-fast問題,該值就是比較的基礎 ++modCount; // size即map中包括k-v數量的多少 // 超過最大容量 就擴容 if (++size > threshold) resize(); // Callbacks to allow LinkedHashMap post-actions afterNodeInsertion(evict); return null; }
HashMap的put方法執行過程整體如下:
①.判斷鍵值對陣列table[i]是否為空或為null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的陣列索引i,如果table[i]==null,直接新建節點新增
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value
④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對
⑤.遍歷table[i],判斷連結串列長度是否大於8,大於8的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
HashMap總結
HashMap底層結構?基於Map介面的實現,陣列+連結串列的結構,JDK 1.8後加入了紅黑樹,連結串列長度>8變紅黑樹,<6變連結串列
兩個物件的hashcode相同會發生什麼? Hash衝突,HashMap通過連結串列來解決hash衝突
HashMap 中 equals() 和 hashCode() 有什麼作用?HashMap 的新增、獲取時需要通過 key 的 hashCode() 進行 hash(),然後計算下標 ( n-1 & hash),從而獲得要找的同的位置。當發生衝突(碰撞)時,利用 key.equals() 方法去連結串列或樹中去查詢對應的節點
HashMap 何時擴容?put的元素達到容量乘負載因子的時候,預設16*0.75
hash 的實現嗎?h = key.hashCode()) ^ (h >>> 16), hashCode 進行無符號右移 16 位,然後進行按位異或,得到這個鍵的雜湊值,由於雜湊表的容量都是 2 的 N 次方,在當前,元素的 hashCode() 在很多時候下低位是相同的,這將導致衝突(碰撞),因此 1.8 以後做了個移位操作:將元素的 hashCode() 和自己右移 16 位後的結果求異或
HashMap執行緒安全嗎?HashMap讀寫效率較高,但是因為其是非同步的,即讀寫等操作都是沒有鎖保護的,所以在多執行緒場景下是不安全的,容易出現資料不一致的問題,在單執行緒場景下非常推薦使用。
以上就是HashMap的介紹。
-END--
關於作者: 陳睿,英文名 mikechen ,十餘年BAT架構經驗,資深技術專家,曾任職阿里、淘寶、百度。
歡迎關注個人公眾號: mikechen的網際網路架構,十餘年BAT架構經驗傾囊相授!
在公眾號選單欄對話方塊回覆【 架構】關鍵詞,即可檢視我原創的 300期+BAT架構技術系列文章與1000+大廠面試題答案合集。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70011997/viewspace-2852690/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Volatile的實現原理(看這篇就夠了)
- 【劍指Offer】Redis 分散式鎖的實現原理看這篇就夠了Redis分散式
- mongoDB看這篇就夠了MongoDB
- Android Fragment看這篇就夠了AndroidFragment
- Oracle索引,看這篇就夠了Oracle索引
- webpack的入門實踐,看這篇就夠了Web
- kafka3.x原理詳解看這篇就夠了Kafka
- OAuth授權|看這篇就夠了OAuth
- Zookeeper入門看這篇就夠了
- 小程式分享,看這篇就夠了
- JavaScript正則,看這篇就夠了JavaScript
- 入門Webpack,看這篇就夠了Web
- Git 看這一篇就夠了Git
- 索引?看這一篇就夠了!索引
- Transformer 看這一篇就夠了ORM
- XLNet預訓練模型,看這篇就夠了!(程式碼實現)模型
- Spring Cloud Config 實現配置中心,看這一篇就夠了SpringCloud
- 代理模式看這一篇就夠了模式
- Java 動態代理,看這篇就夠了Java
- python 操作 mysql 只看這篇就夠了PythonMySql
- 學透 Redis HyperLogLog,看這篇就夠了Redis
- Flutter DataTable 看這一篇就夠了Flutter
- 小程式入門看這篇就夠了
- 關於流量清洗,看這篇就夠了
- Java 集合看這一篇就夠了Java
- vue 元件通訊看這篇就夠了Vue元件
- EFCore 6.0入門看這篇就夠了
- ZooKeeper分散式配置——看這篇就夠了分散式
- java序列化,看這篇就夠了Java
- 想要快速實現告警管理?來看這一篇就夠了!
- 一致性hash原理 看這一篇就夠了
- 關於GC原理和效能調優實踐,看這一篇就夠了!GC
- 前端er瞭解GraphQL,看這篇就夠了前端
- 入門Hbase,看這一篇就夠了
- Python3入門,看這篇就夠了Python
- Redis主從複製看這篇就夠了Redis
- Spring入門看這一篇就夠了Spring
- Mybatis入門看這一篇就夠了MyBatis