JDK1.8對HashMap底層的實現進行了優化,例如引入紅黑樹的數據結構和擴容的優化等。java
簡介node
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方法源碼以下:
public V put(K key, V value) {
// 對key的hashCode()作hash
return putVal(hash(key), key, value, false, true);
}
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爲空則建立
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步驟②:計算index,並對null作處理
if ((p = tab[i = (n - 1) & hash]) == 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;
// 步驟⑥:超過最大容量 就擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
擴容機制
固然Java裏的數組是沒法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像咱們用一個小桶裝水,若是想裝更多的水,就得換大水桶。
鑑於JDK1.8融入了紅黑樹,較複雜,爲了便於理解咱們仍然使用JDK1.7的代碼,好理解一些,本質上區別不大,具體區別後文再說。
void resize(int newCapacity) { //傳入新的容量
Entry[] oldTable = table; //引用擴容前的Entry數組
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //擴容前的數組大小若是已經達到最大(2^30)了
threshold = Integer.MAX_VALUE; //修改閾值爲int的最大值(2^31-1),這樣之後就不會擴容了
return;
}
Entry[] newTable = new Entry[newCapacity]; //初始化一個新的Entry數組
transfer(newTable); //!!將數據轉移到新的Entry數組裏
table = newTable; //HashMap的table屬性引用新的Entry數組
threshold = (int)(newCapacity * loadFactor);//修改閾值
}
transfer()方法將原有Entry數組的元素拷貝到新的Entry數組裏。
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了舊的Entry數組
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組
Entry<K,V> e = src[j]; //取得舊Entry數組的每一個元素
if (e != null) {
src[j] = null;//釋放舊Entry數組的對象引用(for循環後,舊的Entry數組再也不引用任何對象)
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!從新計算每一個元素在數組中的位置
e.next = newTable[i]; //標記[1]
newTable[i] = e; //將元素放在數組上
e = next; //訪問下一個Entry鏈上的元素
} while (e != null);
}
}
}
同一位置上新元素總會被放在鏈表的頭部位置,咱們在擴充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
長按二維碼向我轉帳
受蘋果公司新規定影響,微信 iOS 版的讚揚功能被關閉,可經過二維碼轉帳支持公衆號。