什麼是Map?
Map用於保存具有key-value映射關係的數據
首先看圖!
可以看出Java 中有四種常見的Map實現——HashMap, TreeMap, Hashtable和LinkedHashMap:
·HashMap就是一張hash表,鍵和值都沒有排序。
·TreeMap以紅黑樹結構爲基礎,鍵值可以設置按某種順序排列。
·LinkedHashMap保存了插入時的順序。
·Hashtable是同步的(而HashMap是不同步的)。所以如果在線程安全的環境下應該多使用HashMap,而不是Hashtable,因爲Hashtable對同步有額外的開銷。
我們在這裏簡單的說說HashMap:
(1)HashMap是基於哈希表實現的,每一個元素是一個key-value對,其內部通過單鏈表解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。
(2)HashMap是非線程安全的,只用於單線程環境下,多線程環境下可以採用concurrent併發包下的concurrentHashMap。
(3)HashMap 實現了Serializable接口,因此它支持序列化。
(4)HashMap還實現了Cloneable接口,故能被克隆。
先從HashMap的存儲結構說起:
藍色部分即代表哈希表本身(其實是一個數組),數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決hash地址衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中保存。
HashMap的構造方法中有兩個很重要的參數:初始容量和加載因子
這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是創建哈希表時的容量(默認爲16),加載因子是哈希表當前key的數量和容量的比值,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表提前進行 resize 操作(即擴容)。如果加載因子越大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);如果加載因子太小,那麼表中的數據將過於稀疏(很多空間還沒用,就開始擴容了),嚴重浪費。
JDK開發者規定的默認加載因子爲0.75,因爲這是一個比較理想的值。另外,無論指定初始容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的冪次方,且最大值不能超過2的30次方。
我們來分析一下HashMap中用的最多的兩個方法put和get的源碼
get():
// 獲取key對應的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 獲取key的hash值
int hash = hash(key.hashCode());
// 在“該hash值對應的鏈表”上查找“鍵值等於key”的元素
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
// 判斷key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
// 沒找到則返回null
return null;
}
// 獲取“key爲null”的元素的值,HashMap將“key爲null”的元素存儲在table[0]位置,但不一定是該鏈表的第一個位置!
private V getForNullKey() {
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
首先,如果key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中,當然不一定是存放在頭結點table[0]中。如果key不爲null,則先求的key的hash值,根據hash值找到在table中的索引,在該索引對應的單鏈表中查找是否有鍵值對的key與目標key相等,有就返回對應的value,沒有則返回null。
put():
// 將“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key爲null”,則將該鍵值對添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不爲null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“該key”對應的鍵值對不存在,則將“key-value”添加到table中
modCount++;
// 將key-value添加到table[i]處
addEntry(hash, key, value, i);
return null;
}
如果key爲null,則將其添加到table[0]對應的鏈表中,如果key不爲null,則同樣先求出key的hash值,根據hash值得出在table中的索引,而後遍歷對應的單鏈表,如果單鏈表中存在與目標key相等的鍵值對,則將新的value覆蓋舊的value,且將舊的value返回,如果找不到與目標key相等的鍵值對,或者該單鏈表爲空,則將該鍵值對插入到單鏈表的頭結點位置(每次新插入的節點都是放在頭結點的位置),該操作是有addEntry方法實現的,它的源碼如下:
// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K, V> e = table[bucketIndex];
// 設置“bucketIndex”位置的元素爲“新Entry”,
// 設置“e”爲“新Entry的下一個節點”
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
// 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
注意這裏倒數第三行的構造方法,將key-value鍵值對賦給table[bucketIndex],並將其next指向元素e,這便將key-value放到了頭結點中,並將之前的頭結點接在了它的後面。該方法也說明,每次put鍵值對的時候,總是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。兩外注意最後兩行代碼,每次加入鍵值對時,都要判斷當前已用的槽的數目是否大於等於閥值(容量*加載因子),如果大於等於,則進行擴容,將容量擴爲原來容量的2倍。
接下來重點來分析下求hash值和索引值的方法,這兩個方法便是HashMap設計的最爲核心的部分,二者結合能保證哈希表中的元素儘可能均勻地散列。
由hash值找到對應索引的方法如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
因爲容量初始還是設定都會轉化爲2的冪次。故可以使用高效的位與運算替代模運算。
計算hash值的方法如下:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK 的 HashMap 使用了一個 hash 方法對hash值使用位的操作,使hash值的計算效率很高。爲什麼這樣做?主要是因爲如果直接使用hashcode值,那麼這是一個int值(8個16進制數,共32位),int值的範圍正負21億多,但是hash表沒有那麼長,一般比如初始16,自然散列地址需要對hash表長度取模運算,得到的餘數纔是地址下標。假設某個key的hashcode是0AAA0000,hash數組長默認16,如果不經過hash函數處理,該鍵值對會被存放在hash數組中下標爲0處,因爲0AAA0000 & (16-1) = 0。過了一會兒又存儲另外一個鍵值對,其key的hashcode是0BBB0000,得到數組下標依然是0,這就說明這是個實現得很差的hash算法,因爲hashcode的1位全集中在前16位了,導致算出來的數組下標一直是0。於是明明key相差很大的鍵值對,卻存放在了同一個鏈表裏,導致以後查詢起來比較慢(蛻化爲了順序查找)。故JDK的設計者使用hash函數的若干次的移位、異或操作,把hashcode的“1位”變得“鬆散”,非常巧妙。