Java集合框架源碼解讀(2)——HashMap
在Java Collections Framework的體系中中赦肋,主要有兩個(gè)重要的接口块攒,一個(gè)是List、Set和Queue所屬的Collection佃乘,還有一個(gè)就是Map接口了囱井。在上一篇文章中介紹了List接口,它適用于按數(shù)值索引訪問元素的情形趣避。本文中將介紹的Map則提供了一個(gè)更通用的元素存儲(chǔ)方法庞呕。
Map 集合類用于存儲(chǔ)元素對(稱作“鍵”和“值”)也叫鍵值對(key/value pair),其中每個(gè)鍵映射到一個(gè)值程帕。從概念上而言住练,你可以將 List 看作是具有數(shù)值鍵的 Map。Map接口規(guī)定key值是不能重復(fù)的骆捧,而value值可以重復(fù)澎羞。
Map接口有三種重要的具體實(shí)現(xiàn)類——HashMap髓绽、WeakHashMap和TreeMap敛苇,其中HashMap還有一個(gè)重要的子類LinkedHashMap,它們都是非線程安全的類顺呕,本文將通過分析源碼重點(diǎn)介紹HashMap類枫攀,關(guān)于另外幾個(gè)類的內(nèi)容則留到后續(xù)文章再講。
Map接口的架構(gòu)如下圖所示:
在圖中可以看到株茶,Map接口還有一個(gè)叫做HashTable的實(shí)現(xiàn)類来涨,它是JDK早期的產(chǎn)物,與HashMap實(shí)現(xiàn)基本相似启盛,不過是它是線程安全的蹦掐,由于該容器已經(jīng)過時(shí)技羔,現(xiàn)在基本被棄用,因此在系列文章中就不多加筆墨去介紹了卧抗。
概述
HashMap是基于哈希表實(shí)現(xiàn)的藤滥,HashMap的每一個(gè)元素是一個(gè)key-value對,其內(nèi)部通過單鏈表和紅黑樹解決沖突問題社裆,容量不足時(shí)會(huì)自動(dòng)擴(kuò)容拙绊。
HashMap是非線程安全的,只適用于單線程環(huán)境下泳秀,多線程環(huán)境下可以采用Concurrent并發(fā)包下的ConcurrentHashMap。
哈希沖突
對于每個(gè)對象 X 和 Y,如果當(dāng)且僅當(dāng) X.equals(Y) 為 false卦睹,使得 X.hashCode()!= Y.hashCode() 為 true逗物,這樣的函數(shù)叫做完美 Hash 函數(shù)。當(dāng)哈希函數(shù)對兩個(gè)不同的數(shù)據(jù)項(xiàng)產(chǎn)生了相同的hash值時(shí)磺陡,這就稱為哈希沖突趴梢。
基于對象中變化的字段,我們可以很容易地構(gòu)造一個(gè)完美哈希函數(shù)币他,但是這需要無限的內(nèi)存大小坞靶,這種假設(shè)顯然是不可能的。而且蝴悉,即使我們能夠?yàn)槊總€(gè) POJO(Plain Ordinary Java Object)或者 String 對象構(gòu)造一個(gè)理論上不會(huì)有沖突的哈希函數(shù)彰阴,但是 hashCode() 函數(shù)的返回值是 int 型。根據(jù)鴿籠理論拍冠,當(dāng)我們的對象超過 232 個(gè)時(shí)尿这,這些對象會(huì)發(fā)生哈希沖突。
因此庆杜,實(shí)現(xiàn)HashMap的一個(gè)重要考量射众,就是盡可能地避免哈希沖突。HashMap在JDK 1.8中的做法是晃财,用鏈表和紅黑樹存儲(chǔ)相同hash值的value叨橱。當(dāng)Hash沖突的個(gè)數(shù)比較少時(shí),使用鏈表断盛,否則使用紅黑樹罗洗。
底層實(shí)現(xiàn)
HashMap實(shí)現(xiàn)的接口如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap繼承自抽象類AbstractMap,實(shí)現(xiàn)了Map接口钢猛,AbstractMap類實(shí)現(xiàn)了Map接口的部分方法伙菜,因此Map的最終實(shí)現(xiàn)類直接繼承AbstractMap,可以減少很多工作量命迈。
先來看HashMap內(nèi)部兩個(gè)重要的靜態(tài)內(nèi)部類贩绕。
單向鏈表的節(jié)點(diǎn)Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
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實(shí)現(xiàn)了Map的內(nèi)部接口Entry火的,Entry接口定義了鍵值對(key-value pair)的基本操作,Node類提供了這些方法的實(shí)現(xiàn)并且還含有一個(gè)next引用淑倾,作為單鏈表的實(shí)現(xiàn)用來指向下一個(gè)Node卫玖。
紅黑樹的節(jié)點(diǎn)TreeNode:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/** * Returns root of tree containing this node. */
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
……
}
當(dāng)一個(gè)單鏈表沖突的結(jié)點(diǎn)數(shù)超過預(yù)設(shè)值時(shí),將會(huì)把這個(gè)單鏈表自動(dòng)調(diào)整為紅黑樹踊淳。這樣做的好處是假瞬,最壞的情況下即所有的key都Hash沖突,采用鏈表的話查找時(shí)間為O(n),而采用紅黑樹為O(logn)迂尝。
HashMap的幾個(gè)重要字段如下:
//存儲(chǔ)數(shù)據(jù)的Node數(shù)組脱茉,長度是2的冪。
transient Node<K,V>[] table;
//鍵值對緩存垄开,它們的映射關(guān)系集合保存在entrySet中琴许。即使Key在外部修改導(dǎo)致hashCode變化,緩存中還可以找到映射關(guān)系
transient Set<Map.Entry<K,V>> entrySet;
//map中保存的鍵值對的數(shù)量
transient int size;
//map結(jié)構(gòu)被改變的次數(shù)
transient int modCount;
//需要調(diào)整大小的極限值(容量*裝載因子)
int threshold;
//裝載因子溉躲,在后面會(huì)進(jìn)行詳細(xì)介紹
final float loadFactor;
HashMap內(nèi)部使用Node數(shù)組實(shí)現(xiàn)了一個(gè)哈希桶數(shù)組table榜田。可以看出锻梳,HashMap還是憑借數(shù)組實(shí)現(xiàn)的箭券,數(shù)組的元素是單鏈表或紅黑樹,對于key的hash值相等的key-value pair疑枯,它們將分別作為一個(gè)結(jié)點(diǎn)(Node或TreeNode)存儲(chǔ)在同一個(gè)單鏈表或紅黑樹中辩块。
我們知道數(shù)組的特點(diǎn):尋址容易,插入和刪除困難荆永,而鏈表的特點(diǎn)是:尋址困難废亭,插入和刪除容易,紅黑樹則對插入時(shí)間具钥、刪除時(shí)間和查找時(shí)間提供了最好可能的最壞情況擔(dān)保豆村。HashpMap將這三者結(jié)合在一起。
HashMap的數(shù)據(jù)結(jié)構(gòu)如下圖所示:
此外骂删,這里的modCount屬性掌动,記錄了map結(jié)構(gòu)被改變的次數(shù),它與“fail-fast”機(jī)制的實(shí)現(xiàn)息息相關(guān)桃漾。fail-fast機(jī)制是Java集合的一種錯(cuò)誤檢測機(jī)制坏匪,假設(shè)存在兩個(gè)線程(線程1拟逮、線程2)撬统,線程1通過Iterator在遍歷集合A中的元素,在某個(gè)時(shí)候線程2修改了集合A的結(jié)構(gòu)(是結(jié)構(gòu)上面的修改敦迄,而不是簡單的修改集合元素的內(nèi)容)恋追,那么這個(gè)時(shí)候程序就會(huì)拋出 ConcurrentModificationException 異常凭迹,從而產(chǎn)生fail-fast機(jī)制。
對于HashMap內(nèi)容的修改都將使modCount的值增加苦囱,在迭代器初始化過程中會(huì)將這個(gè)值賦給迭代器的expectedModCount,在迭代過程中嗅绸,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經(jīng)有其他線程修改了Map撕彤。
HashMap的一些重要的靜態(tài)全局變量如下鱼鸠,它們與HashMap規(guī)避哈希碰撞的策略息息相關(guān):
/** * table默認(rèn)的初始容量,它的值必須是2的整數(shù)冪 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * table的最大容量羹铅,必須小于2的30次方蚀狰,如果傳入的容量大于這個(gè)值,將被替換為該值 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/** * 默認(rèn)裝載因子职员,如果在構(gòu)造函數(shù)中不顯式指定裝載因子麻蹋,則默認(rèn)使用該值。 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * 結(jié)點(diǎn)沖突數(shù)達(dá)到8時(shí)焊切,就會(huì)對哈希表進(jìn)行調(diào)整扮授,如果table容量小于64,那么會(huì)進(jìn)行擴(kuò)容专肪, * 如果不小于64刹勃,那么會(huì)將沖突數(shù)達(dá)到8的那個(gè)單鏈表調(diào)整為紅黑樹. */
static final int TREEIFY_THRESHOLD = 8;
/** * 如果原先就是紅黑樹,resize以后沖突結(jié)點(diǎn)數(shù)少于6了嚎尤,就把紅黑色恢復(fù)成單鏈表 */
static final int UNTREEIFY_THRESHOLD = 6;
/** * 如果table的容量少于64深夯,那么即使沖突結(jié)點(diǎn)數(shù)達(dá)到TREEIFY_THRESHOLD后不會(huì)把該單鏈表調(diào)整成紅黑數(shù),而是將table擴(kuò)容 */
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap使用的hash算法如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
使用hash值的高位16位與低16進(jìn)行XORs操作诺苹,算法簡潔有效咕晋。
常用API
看完了HashMap的基本數(shù)據(jù)結(jié)構(gòu)以后,來看一下常用方法的源碼收奔,首先自然想到的是get(key)
和put(key,value)
掌呜。
get(key)
get(key)
方法的作用是的源碼如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
我們將要查找的key值傳給get,它調(diào)用hashCode
計(jì)算hash從而得到bucket位置坪哄,并進(jìn)一步調(diào)用equals()
方法確定鍵值對质蕉。取模算法中的除法運(yùn)算效率很低,在HashMap中通過h & (n-1)替代取模翩肌,得到所在數(shù)組位置模暗,效率會(huì)高很多(前提是保證數(shù)組的容量是2的整數(shù)倍)。
resize()
在介紹put
方法之前還要先來看一下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;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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) {
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 { // preserve order
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) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
當(dāng)HashMap中的元素個(gè)數(shù)超過 數(shù)組大小 * loadFactor 時(shí)兑宇,就會(huì)進(jìn)行數(shù)組擴(kuò)容,loadFactor的默認(rèn)值為0.75粱坤,這是一個(gè)折中的取值隶糕。也就是說瓷产,默認(rèn)情況下,數(shù)組大小為16枚驻,那么當(dāng)HashMap中元素個(gè)數(shù)超過 16 * 0.75=12 的時(shí)候濒旦,就把數(shù)組的大小擴(kuò)展為 2 * 16=32 ,即擴(kuò)大一倍再登,然后重新計(jì)算每個(gè)元素在數(shù)組中的位置尔邓,而這是一個(gè)非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知HashMap中元素的個(gè)數(shù)锉矢,那么預(yù)設(shè)元素的個(gè)數(shù)能夠有效的提高HashMap的性能铃拇。
put(key,value)
put(key,value)
方法的作用是向HashMap中添加一對key-value pair。源碼如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
}
將key-value pair傳給put
方法時(shí)沈撞,它調(diào)用hashCode
計(jì)算hash從而得到bucket位置慷荔,進(jìn)而,HashMap根據(jù)當(dāng)前bucket的占用情況自動(dòng)調(diào)整容量(超過Load Factor則resize為原來的2倍)缠俺。
如果沒有發(fā)生碰撞就直接放到bucket里显晶,如果發(fā)生碰撞,Hashmap先通過鏈表將產(chǎn)生碰撞沖突的元素組織起來壹士,如果一個(gè)bucket中碰撞沖突的元素超過某個(gè)限制(默認(rèn)是8)磷雇,則使用紅黑樹來替換鏈表,從而提高速度躏救。