Java集合框架之HashMap的實(shí)現(xiàn)原理

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ò)誤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蕉毯,一起剝皮案震驚了整個(gè)濱河市乓搬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恕刘,老刑警劉巖缤谎,帶你破解...
    沈念sama閱讀 212,294評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異褐着,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)托呕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)含蓉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人项郊,你說(shuō)我怎么就攤上這事馅扣。” “怎么了着降?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,790評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵差油,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我任洞,道長(zhǎng)蓄喇,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,595評(píng)論 1 284
  • 正文 為了忘掉前任交掏,我火速辦了婚禮妆偏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盅弛。我一直安慰自己钱骂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,718評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布挪鹏。 她就那樣靜靜地躺著见秽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讨盒。 梳的紋絲不亂的頭發(fā)上解取,一...
    開(kāi)封第一講書(shū)人閱讀 49,906評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音催植,去河邊找鬼肮蛹。 笑死勺择,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伦忠。 我是一名探鬼主播省核,決...
    沈念sama閱讀 39,053評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼昆码!你這毒婦竟也來(lái)了气忠?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,797評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赋咽,失蹤者是張志新(化名)和其女友劉穎旧噪,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體脓匿,經(jīng)...
    沈念sama閱讀 44,250評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淘钟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,570評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陪毡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片米母。...
    茶點(diǎn)故事閱讀 38,711評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖毡琉,靈堂內(nèi)的尸體忽然破棺而出铁瞒,到底是詐尸還是另有隱情,我是刑警寧澤桅滋,帶...
    沈念sama閱讀 34,388評(píng)論 4 332
  • 正文 年R本政府宣布慧耍,位于F島的核電站,受9級(jí)特大地震影響丐谋,放射性物質(zhì)發(fā)生泄漏芍碧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,018評(píng)論 3 316
  • 文/蒙蒙 一笋鄙、第九天 我趴在偏房一處隱蔽的房頂上張望师枣。 院中可真熱鬧,春花似錦萧落、人聲如沸践美。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,796評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)陨倡。三九已至,卻和暖如春许布,著一層夾襖步出監(jiān)牢的瞬間蜕该,已是汗流浹背氧枣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工光戈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筹陵,地道東北人覆履。 一個(gè)月前我還...
    沈念sama閱讀 46,461評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咱揍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,595評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容