JDK1.8對HashMap底層的實現進行了優化,例如引入紅黑樹的數據結構和擴容的優化等
簡介
Java為數據結構中的映射定義了一個接口java.util.Map
-
HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度。
HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。非線程安全。
如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap -
Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類。線程安全。並發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。
-
LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。
-
TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。
在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。
內部實現
(1) 存儲結構-字段
(2) 功能實現-方法
存儲結構-字段
HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的。
這里需要講明白兩個問題:數據底層具體存儲的是什么?這樣的存儲方式有什么優點呢?
- HashMap類中有一個非常重要的字段,就是 Node[] table,即哈希桶數組
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) { ... }
}
Node是HashMap的一個內部類,實現了Map.Entry接口,本質是就是一個映射(鍵值對)。上圖中的每個黑色圓點就是一個Node對象。
- HashMap就是使用哈希表來存儲的。Java中HashMap采用了拉鏈法解決沖突。
例如程序執行下面代碼:
map.put("美團","小美");
系統將調用"美團"這個key的hashCode()方法得到其hashCode 值(該方法適用於每個Java對象),然后再通過Hash算法的后兩步運算(高位運算和取模運算,下文有介紹)來定位該鍵值對的存儲位置。
哈希桶數組需要在空間成本和時間成本之間權衡。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。
HashMap的默認構造函數就是對下面幾個字段進行初始化
int threshold; // 所能容納的key-value對極限
final float loadFactor; // 負載因子
int modCount; // 用來記錄HashMap內部結構發生變化的次數
int size;
Node[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Node(鍵值對)個數。
threshold就是在此Load factor和length(數組長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容后的HashMap容量是之前容量的兩倍。
在HashMap中,哈希桶數組table的長度length大小必須為2的n次方(一定是合數),這是一種非常規的設計,常規的設計是把桶的大小設計為素數。相對來說素數導致沖突的概率要小於合數[2].
HashMap采用這種非常規設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。
當鏈表長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能。
功能實現-方法
HashMap的內部功能實現很多,本文主要講述:
- 根據key獲取哈希桶數組索引位置
- put方法的詳細執行
- 擴容過程
確定哈希桶數組索引位置
先看看源碼的實現(方法一+方法二):
方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 為第一步 取hashCode值
// h ^ (h >>> 16) 為第二步 高位參與運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個方法,但是實現原理一樣的
return h & (length-1); //第三步 取模運算
}
這里的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
只要它的hashCode()返回值相同,那么程序調用方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,模運算的消耗還是比較大的,在HashMap中是這樣做的:調用方法二來計算該對象應該保存在table數組的哪個索引處。
而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。
分析HashMap的put方法
JDK1.8HashMap的put方法源碼如下:
1 public V put(K key, V value) {
2 // 對key的hashCode()做hash
3 return putVal(hash(key), key, value, false, true);
4 }
5
6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node<K,V>[] tab; Node<K,V> p; int n, i;
9 // 步驟①:tab為空則創建
10 if ((tab = table) == null || (n = tab.length) == 0)
11 n = (tab = resize()).length;
12 // 步驟②:計算index,並對null做處理
13 if ((p = tab[i = (n - 1) & hash]) == null)
14 tab[i] = newNode(hash, key, value, null);
15 else {
16 Node<K,V> e; K k;
17 // 步驟③:節點key存在,直接覆蓋value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步驟④:判斷該鏈為紅黑樹
22 else if (p instanceof TreeNode)
23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24 // 步驟⑤:該鏈為鏈表
25 else {
26 for (int binCount = 0; ; ++binCount) {
27 if ((e = p.next) == null) {
28 p.next = newNode(hash, key,value,null);
//鏈表長度大於8轉換為紅黑樹進行處理
29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30 treeifyBin(tab, hash);
31 break;
32 }
// key已經存在直接覆蓋value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k)))) break;
36 p = e;
37 }
38 }
39
40 if (e != null) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = value;
44 afterNodeAccess(e);
45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步驟⑥:超過最大容量 就擴容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }
擴容機制
當然Java里的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。
鑒於JDK1.8融入了紅黑樹,較復雜,為了便於理解我們仍然使用JDK1.7的代碼,好理解一些,本質上區別不大,具體區別后文再說。
1 void resize(int newCapacity) { //傳入新的容量
2 Entry[] oldTable = table; //引用擴容前的Entry數組
3 int oldCapacity = oldTable.length;
4 if (oldCapacity == MAXIMUM_CAPACITY) { //擴容前的數組大小如果已經達到最大(2^30)了
5 threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了
6 return;
7 }
8
9 Entry[] newTable = new Entry[newCapacity]; //初始化一個新的Entry數組
10 transfer(newTable); //!!將數據轉移到新的Entry數組里
11 table = newTable; //HashMap的table屬性引用新的Entry數組
12 threshold = (int)(newCapacity * loadFactor);//修改閾值
13 }
transfer()方法將原有Entry數組的元素拷貝到新的Entry數組里。
1 void transfer(Entry[] newTable) {
2 Entry[] src = table; //src引用了舊的Entry數組
3 int newCapacity = newTable.length;
4 for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組
5 Entry<K,V> e = src[j]; //取得舊Entry數組的每個元素
6 if (e != null) {
7 src[j] = null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)
8 do {
9 Entry<K,V> next = e.next;
10 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置
11 e.next = newTable[i]; //標記[1]
12 newTable[i] = e; //將元素放在數組上
13 e = next; //訪問下一個Entry鏈上的元素
14 } while (e != null);
15 }
16 }
17 }
同一位置上新元素總會被放在鏈表的頭部位置
我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了
(這部分並沒有完全懂)
線程安全
HashMap在多線程的情況下可能鏈結構會受到破壞,導致無限循壞(JDK8 可能已經解決)
小結
(1) 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
(2) 負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。
(3) HashMap是線程不安全的,不要在並發的環境中同時操作HashMap,建議使用ConcurrentHashMap。
(4) JDK1.8引入紅黑樹大程度優化了HashMap的性能。
參考資料:
- 美團點評技術團隊 Java 8系列之重新認識HashMap https://zhuanlan.zhihu.com/p/21673805
- 為什么一般hashtable的桶數會取一個素數 http://blog.csdn.net/liuqiyao_01/article/details/14475159