HashMap概述
HashMap 是基于哈希表的 Map 接口的非同步實(shí)現(xiàn)暇屋。此實(shí)現(xiàn)提供所有可選的映射操作柒昏, 并允許使用 null 值作為鍵值對(duì)的 Key 和 Value 苦蒿。此類(lèi)不保證映射的順序儿捧,特別是它不保證該順序恒久不變。
HashMap的數(shù)據(jù)結(jié)構(gòu)
在 Java 編程語(yǔ)言中墨闲,最基本的結(jié)構(gòu)就是兩種今妄,一個(gè)是數(shù)組,另外一個(gè)是模擬指針(即 引用)鸳碧,所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個(gè)基本結(jié)構(gòu)來(lái)構(gòu)造的盾鳞,HashMap 也不例外。HashMap 實(shí)際上是一個(gè)“鏈表散列”的數(shù)據(jù)結(jié)構(gòu)瞻离,即數(shù)組和鏈表的結(jié)合體雁仲。
HashMap 底層就是一個(gè)數(shù)組結(jié)構(gòu),數(shù)組中的每一項(xiàng)又是一個(gè)鏈表琐脏。 當(dāng)新建一個(gè) HashMap 的時(shí)候,就會(huì)初始化一個(gè)數(shù)組。
HashMap的實(shí)現(xiàn)源碼如下:
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
可以看出日裙,Entry 就是數(shù)組中的元素吹艇,每個(gè) Map.Entry 其實(shí)就是一個(gè) key-value 對(duì),它 持有一個(gè)指向下一個(gè)元素的引用昂拂,這就構(gòu)成了鏈表受神。
HashMap存取實(shí)現(xiàn)
1.存儲(chǔ)
public V put(K key, V value) {
// HashMap 允許存放 null 鍵和 null 值
// 當(dāng) key 為 null 時(shí),調(diào)用 putForNullKey 方法格侯,將 value 放置在數(shù)組第一個(gè)位置鼻听。
if (key == null)
return putForNullKey(value);
// 根據(jù) key 的 keyCode 重新計(jì)算 hash 值。
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;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果 i 索引處的 Entry 為 null联四,表明此處還沒(méi)有 Entry撑碴。
modCount++;
addEntry(hash, key, value, i);
return null;
}
從源代碼中可以看出:當(dāng)我們往 HashMap 中 put 元素的時(shí)候,先根據(jù) key 的 hashCode 重新計(jì)算 hash 值朝墩,根據(jù) hash 值得到這個(gè)元素在數(shù)組中的位置(即 下標(biāo))醉拓,如果數(shù)組該位置上已經(jīng)存放有其他元素了,那么在這個(gè)位置上的元素將以鏈表的形式存放收苏,新加入的放在鏈頭亿卤,最先加入的放在鏈尾。如果數(shù)組該位置上沒(méi)有元素鹿霸,就直接將該元素放到此數(shù)組中的該位置上排吴。
addEntry(hash, key, value, i)方法根據(jù)計(jì)算出的 hash 值,將 key-value 對(duì)放在數(shù)組 table 的 i 索引處懦鼠。addEntry 是 HashMap 提供的一個(gè)包訪問(wèn)權(quán)限的方法钻哩,代碼如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
// 獲取指定 bucketIndex 索引處的 Entry
Entry<K,V> e = table[bucketIndex];
// 將新創(chuàng)建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來(lái)的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 對(duì)的數(shù)量超過(guò)了極限
if (size++ >= threshold)
// 把 table 對(duì)象的長(zhǎng)度擴(kuò)充到原來(lái)的 2 倍
resize(2 * table.length);
}
當(dāng)系統(tǒng)決定存儲(chǔ) HashMap 中的 key-value 對(duì)時(shí)葛闷,完全沒(méi)有考慮 Entry 中的 value憋槐,僅僅只是根據(jù)key來(lái)計(jì)算并決定每個(gè)Entry的存儲(chǔ)位置。我們完全可以把 Map 集合中的 value 當(dāng)成 key 的附屬淑趾,當(dāng)系統(tǒng)決定了 key 的存儲(chǔ)位置之后阳仔,value 隨之保存在那里即可。
hash(int h)方法根據(jù) key 的 hashCode 重新計(jì)算一次散列扣泊。此算法加入了高位計(jì)算近范,防 止低位不變,高位變化時(shí)延蟹,造成的 hash 沖突评矩。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
可以看到在 HashMap 中要找到某個(gè)元素,需要根據(jù) key 的 hash 值來(lái)求得對(duì)應(yīng)數(shù) 組中的位置阱飘。如何計(jì)算這個(gè)位置就是 hash 算法斥杜。前面說(shuō)過(guò) HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和 鏈表的結(jié)合虱颗,所以我們當(dāng)然希望這個(gè) HashMap 里面的 元素位置盡量的分布均勻些,盡量 使得每個(gè)位置上的元素?cái)?shù)量只有一個(gè)蔗喂,那么當(dāng)我們用 hash 算法求得這個(gè)位置的時(shí)候忘渔,馬上就可以知道對(duì)應(yīng)位置的元素就是我們要的,而不用再去遍歷鏈表缰儿,這樣就大大優(yōu)化了查詢的 效率畦粮。
對(duì)于任意給定的對(duì)象,只要它的 hashCode() 返回值相同乖阵,那么程序調(diào)用 hash(int h) 方 法所計(jì)算得到的 hash 碼值總是相同的宣赔。我們首先想到的就是把 hash 值對(duì)數(shù)組長(zhǎng)度取模運(yùn) 算,這樣一來(lái)瞪浸,元素的分布相對(duì)來(lái)說(shuō)是比較均勻的儒将。但是,“哪眨”運(yùn)算的消耗還是比較大的椅棺, 在 HashMap 中是這樣做的:調(diào)用 indexFor(int h, int length) 方法來(lái)計(jì)算該對(duì)象應(yīng)該保存 在 table 數(shù)組的哪個(gè)索引處。indexFor(int h, int length) 方法的代碼如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
這個(gè)方法非常巧妙齐蔽,它通過(guò) h & (table.length -1) 來(lái)得到該對(duì)象的保存位两疚,而 HashMap 底層數(shù)組的長(zhǎng)度總是 2 的 n 次方,這是 HashMap 在速度上的優(yōu)化含滴。在 HashMap 構(gòu)造器中 有如下代碼:
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
這段代碼保證初始化時(shí) HashMap 的容量總是 2 的 n 次方诱渤,即底層數(shù)組的長(zhǎng)度總是為 2 的 n 次方。當(dāng) length 總是 2 的 n 次方時(shí)谈况,h& (length-1)運(yùn)算等價(jià)于對(duì) length 取模勺美,也就是 h%length,但是&比%具有更高的效率碑韵。
2.讀取
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null;e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
}
從 HashMap 中 get 元素時(shí)赡茸,首先計(jì)算 key 的 hashCode,找到數(shù)組中對(duì)應(yīng) 位置的某一元素祝闻,然后通過(guò) key 的 equals 方法在對(duì)應(yīng)位置的鏈表中找到需要的元素占卧。
歸納起來(lái)簡(jiǎn)單地說(shuō),HashMap 在底層將 key-value 當(dāng)成一個(gè)整體進(jìn)行處理联喘,這個(gè)整體 就是一個(gè) Entry 對(duì)象华蜒。HashMap 底層采用一個(gè) Entry[] 數(shù)組來(lái)保存所有的 key-value 對(duì),當(dāng) 需要存儲(chǔ)一個(gè) Entry 對(duì)象時(shí)豁遭,會(huì)根據(jù)hash算法來(lái)決定其在數(shù)組中的存儲(chǔ)位置叭喜,在根據(jù)equals 方法決定其在該數(shù)組位置上的鏈表中的存儲(chǔ)位置;當(dāng)需要取出一個(gè) Entry 時(shí)蓖谢,也會(huì)根據(jù) hash 算法找到其在數(shù)組中的存儲(chǔ)位置捂蕴,再根據(jù) equals 方法從該位置上的鏈表中取出該 Entry譬涡。
HashMap 的 resize(rehash)
當(dāng) HashMap 中的元素越來(lái)越多的時(shí)候,hash 沖突的幾率也就越來(lái)越高启绰,因?yàn)閿?shù)組的 長(zhǎng)度是固定的昂儒。所以為了提高查詢的效率,就要對(duì) HashMap 的數(shù)組進(jìn)行擴(kuò)容委可,數(shù)組擴(kuò)容這 個(gè)操作也會(huì)出現(xiàn)在 ArrayList 中,這是一個(gè)常用的操作腊嗡,而在 HashMap 數(shù)組擴(kuò)容之后着倾,最消耗性能的點(diǎn)就出現(xiàn)了:原數(shù)組中的數(shù)據(jù)必須重新計(jì)算其在新數(shù)組中的位置,并放進(jìn)去燕少,這就是 resize卡者。
當(dāng) HashMap 中的元素個(gè)數(shù)超過(guò)數(shù)組大小×loadFactor 時(shí),就會(huì)進(jìn)行數(shù)組擴(kuò)容客们,loadFactor 的默認(rèn)值為 0.75崇决,這是一個(gè)折中的取值。 也就是說(shuō)底挫,默認(rèn)情況下恒傻,數(shù)組大小為 16,那么當(dāng) HashMap 中元素個(gè)數(shù)超過(guò) 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 的性能注簿。
HashMap 的性能參數(shù)
HashMap 包含如下幾個(gè)構(gòu)造器:
HashMap():構(gòu)建一個(gè)初始容量為 16契吉,負(fù)載因子為 0.75 的 HashMap。
HashMap(int initialCapacity):構(gòu)建一個(gè)初始容量為 initialCapacity诡渴,負(fù)載因子 為 0.75 的 HashMap捐晶。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負(fù)載因子創(chuàng)建一 個(gè) HashMap玩徊。
initialCapacity:HashMap 的最大容量租悄,即為底層數(shù)組的長(zhǎng)度。
loadFactor:負(fù)載因子 loadFactor 定義為:散列表的實(shí)際元素?cái)?shù)目(n)/ 散列表的容量(m)恩袱。
負(fù)載因子衡量的是一個(gè)散列表的空間的使用程度泣棋,負(fù)載因子越大表示散列表的裝填程度越 高,反之愈小畔塔。對(duì)于使用鏈表法的散列表來(lái)說(shuō)潭辈,查找一個(gè)元素的平均時(shí)間是 O(1+a)鸯屿,因此
如果負(fù)載因子越大,對(duì)空間的利用更充分把敢,然而后果是查找效率的降低寄摆;如果負(fù)載因子太小, 那么散列表的數(shù)據(jù)將過(guò)于稀疏修赞,對(duì)空間造成嚴(yán)重浪費(fèi)婶恼。
HashMap 的實(shí)現(xiàn)中,通過(guò) threshold 字段來(lái)判斷 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
結(jié)合負(fù)載因子的定義公式可知柏副,threshold 就是在此 loadFactor 和 capacity 對(duì)應(yīng)下允許的 最大元素?cái)?shù)目勾邦,超過(guò)這個(gè)數(shù)目就重新 resize,以降低實(shí)際的負(fù)載因子割择。默認(rèn)的的負(fù)載因子 0.75是對(duì)空間和時(shí)間效率的一個(gè)平衡選擇眷篇。當(dāng)容量超出此最大容量時(shí), resize后的HashMap 容量是容量的兩倍:
if (size++ >= threshold)
resize(2 * table.length);
Fail-Fast 機(jī)制
java.util.HashMap 不是線程安全的荔泳,因此如果在使用迭代器的過(guò)程中有其他 線程修改了map蕉饼,那么將拋出 ConcurrentModificationException,這就是所謂fail-fast策略玛歌。 這一策略在源碼中的實(shí)現(xiàn)是通過(guò) modCount 域昧港,modCount 顧名思義就是修改次數(shù),對(duì) HashMap 內(nèi)容的修改都將增加這個(gè)值沾鳄,那么在迭代器初始化過(guò)程中會(huì)將這個(gè)值賦給迭代器的 expectedModCount慨飘。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
在迭代過(guò)程中,判斷 modCount 跟 expectedModCount 是否相等译荞,如果不相等就表示已 經(jīng)有其他線程修改了 Map: 注意到 modCount 聲明為 volatile瓤的,保證線程之間修改的可見(jiàn)性。
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
由所有 HashMap 類(lèi)的“collection 視圖方法”所返回的迭代器都是快速失敗的:在迭代器 創(chuàng)建之后吞歼,如果從結(jié)構(gòu)上對(duì)映射進(jìn)行修改圈膏,除非通過(guò)迭代器本身的 remove 方法,其他任何 時(shí)間任何方式的修改篙骡,迭代器都將拋出 ConcurrentModificationException稽坤。因此,面對(duì)并發(fā)的修改糯俗,迭代器很快就會(huì)完全失敗尿褪,而不冒在將來(lái)不確定的時(shí)間發(fā)生任意不確定行為的風(fēng)險(xiǎn)。
注意得湘,迭代器的快速失敗行為不能得到保證杖玲,一般來(lái)說(shuō),存在非同步的并發(fā)修改時(shí)淘正,不可能作出任何堅(jiān)決的保證摆马【饰牛快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。因此囤采,編寫(xiě)依賴于此異常的程序的做法是錯(cuò)誤的述呐,正確做法是:迭代器的快速失敗行為應(yīng)該僅用于檢測(cè)程序錯(cuò)誤。