HashMap面試寶典

前言

本文源碼分析基于jdk1.8版本(持續(xù)更新中)

1疮绷、HashMap數(shù)據(jù)結(jié)構(gòu)與工作原理

這是基礎中的基礎,這個都不能掌握滩字,面試大概率要翻車。源碼自己看御吞,這里講流程麦箍。

HashMap數(shù)據(jù)結(jié)構(gòu).png

在Jdk1.8中,HashMap數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表+紅黑樹陶珠,數(shù)組也叫做hash表挟裂,每條鏈表也叫做桶(bucket),紅黑樹是為了提高查詢效率揍诽。

  • 1诀蓉、hashmap這個數(shù)組也叫做hash桶(bucket),
  • 2暑脆、存放元素的時候會先根據(jù)key的hash值去計算元素下標渠啤,如果這個下標沒有元素,就創(chuàng)建一個Node節(jié)點放進去添吗;
  • 3沥曹、如果數(shù)組下標有數(shù)據(jù),先判斷key是否相同碟联,相同的話替換元素的value妓美;不同的話插在鏈表的尾節(jié)點。注意:同一個鏈表中的節(jié)點只是說數(shù)組下標相同鲤孵,但不一定是發(fā)生了hash沖突的壶栋,有可能hash值不同;
  • 4普监、鏈表長度大于8贵试,且數(shù)組長度大于64,會把鏈表轉(zhuǎn)位紅黑樹凯正,紅黑樹本質(zhì)是一顆自平衡的二叉查找樹毙玻,查找時間復雜度為o(logn);
  • 5漆际、元素容量size超過閾值會擴容淆珊;

下面這張美團技術畫的圖可以很清晰的表達整個流程。


工作流程(美團).png

2奸汇、HashMap如何解決hash碰撞(hash沖突)的施符?

拉鏈法。當存儲元素出現(xiàn)hash沖突時擂找,意味著hash值相同的多個元素要存儲在數(shù)組中的同一個位置戳吝,這時候就通過一個單鏈表來解決,每次新增的元素插在尾節(jié)點贯涎。注意:在同一個鏈表中的元素不能給說明一定是沖突的听哭,有可能hash值不相同。

3塘雳、為什么數(shù)組容量必須是2^n(初始化和擴容)陆盘?

為了讓添加的元素均勻分布在HashMap的數(shù)組上,減少hash碰撞败明。

//put()隘马,計算存儲的元素的下標
//n是數(shù)組長度,默認16
i =  hash  & (n - 1)

這種求下標的做法和hash % n取模運算是一樣的妻顶,只是說&運算是操作的二進制數(shù)酸员,在計算效率上更高一些,反正源碼都很喜歡這種位運算讳嘱。

我們在計算下標的時候當然是希望盡可能讓元素分散到0~n-1位置幔嗦,這樣可以減少沖突,讓查詢效率更高沥潭。下面就來看一下HashMap是怎么做到的邀泉。

hash是int類型,轉(zhuǎn)換為2進制數(shù)是32位钝鸽,為了簡化呼渣,假設
hash=0101 0101,n-1= 15

image.png

這樣就可以限制&運算的結(jié)果在0000~1111之間寞埠,轉(zhuǎn)為10進制數(shù)就是0~15屁置,是不是和求余運算的結(jié)果一致?如果n=17仁连,n-1=0001 0000蓝角,這樣&運算結(jié)果的低位全為0,數(shù)組中有很多位置利用不到饭冬,這樣會出現(xiàn)大量的hash沖突使鹅。

結(jié)論:只有數(shù)組長度為2^n,才能保證n-1的低位的值全為1昌抠,這樣元素就可以更均勻的分散在數(shù)組上患朱。

4、擾動函數(shù)

為了散列效果更好炊苫,減少碰撞裁厅,減少沖突冰沙。

在上面的&運算中,盡管已經(jīng)讓元素更分散了执虹,但是還是存在一個問題拓挥,由于n-1的高位全為0,所以&運算的結(jié)果只和hash的低位有關袋励,這樣的話侥啤,發(fā)生hash沖突的次數(shù)會比較多。但是我們看HashMap源碼茬故,會發(fā)現(xiàn)已經(jīng)通過重寫hash方法優(yōu)化了這一點盖灸。

//計算key的hash值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這里并沒有直接使用Object的hashCode()方法,而是重寫了這個散列方法磺芭。有些同學可能不太看得懂這里的位運算赁炎,我就給大家拆解開來看一下。

   static int hash2(Object key) {
        if ((key == null)) return 0;
        int h = key.hashCode(); //計算hash值
        int high = h >>> 16; //右移16位,那就只保留了h的高16位
        int newHash = h ^ high; //異或運算徘跪,相同為0甘邀,不同為1
        return newHash;
    }

這樣的話,大家應該能看懂了吧垮庐。下面借用一張圖來說明上面的計算松邪。


image.png

h >>> 16只保留了h的高16位,h ^ high是讓h的高16位與低16位做異或運算哨查,這樣高16位與低16位都參與了hash的運算逗抑,使hash值更加不確定 降低了hash碰撞的概率。

5寒亥、樹化的條件是什么邮府?

網(wǎng)上很多具有誤導性的文章說鏈表長度大于8就會轉(zhuǎn)為紅黑樹,實際上是錯誤的溉奕。樹化其實需要2個條件褂傀,鏈表長度 >=7且數(shù)組長度>= 64

存放元素樹化.png
數(shù)組長度大于64才可以樹化.png

6、HashMap擴容是怎么做的加勤?

擴容有3個觸發(fā)時機仙辟,一個是初始化,也就是第一次put()存放數(shù)據(jù)時鳄梅,另一個是存儲的元素數(shù)量大于閾值threshold時叠国;還有一個是樹化的時候(這一點很多人應該不知道),最后都是調(diào)用resize()方法完成擴容和數(shù)據(jù)遷移的戴尸。

如果你沒看過hashmap擴容實現(xiàn)粟焊,你猜擴容是怎么實現(xiàn)的?難道和ArrayList一樣,數(shù)組拷貝项棠,把元素照般到新的數(shù)組中相同的位置就好了悲雳?實際上不是的,原來數(shù)組中的元素在擴容后只有2種選擇沾乘,第一怜奖,在原來的位置浑测;第二翅阵,在原來位置基礎上再加上原來數(shù)組長度。這里先說結(jié)論迁央,后面再源碼分析掷匠。

再來回顧下,我們是通過如下方式計算元素下標的岖圈。記住一點讹语,&運算算法:2個都是1,結(jié)果才為1蜂科,否則為0顽决。

//n是數(shù)組長度,默認16
i =  hash  & (n - 1)

下面這幅圖是擴容前后A导匣、B元素的數(shù)組下標的計算過程(有區(qū)別的地方做了標示)才菠。在擴容前A、B的hash值不一樣贡定,但是&運算后的下標卻是一樣的赋访;擴容后發(fā)生了一個變化,就是n變成了2原來的2倍缓待,變成2倍可以用左移1位表示蚓耽,也就是從0000 1111(16)變成0001 1111(32),那擴容后與運算旋炒,A在高位的第4位&運算結(jié)果為0步悠;B在高位的第4位&運算結(jié)果為1;也就是說A還是在原來的位置瘫镇,B在原來的位置(5),再往后移動16位鼎兽,也就是B移動到21了。

元素下標改變.png

這里的思路很巧妙汇四,利用了移位運算和&運算接奈,n-1的值擴容后會向左移一位,那只需要看看原來的hash值中和這個新增1相同位置的值是1還是0就好了通孽,是0的話下標沒變序宦,是1的話下標變成“原下標+oldCap"。

圖解元素移動.png

源碼解析

final Node<K,V>[] resize() {
   // oldTab 指向舊的 table 表
   Node<K,V>[] oldTab = table;
   // oldCap 代表擴容前 table 表的數(shù)組長度背苦,oldTab 第一次添加元素的時候為 null 
   int oldCap = (oldTab == null) ? 0 : oldTab.length;
   // 舊的擴容閾值
   int oldThr = threshold;
   // 初始化新的閾值和容量
   int newCap, newThr = 0;
   // 如果 oldCap > 0 則會將新容量擴大到原來的2倍互捌,擴容閾值也將擴大到原來閾值的兩倍
   if (oldCap > 0) {
       // 如果舊的容量已經(jīng)達到最大容量 2^30 那么就不在繼續(xù)擴容直接返回潘明,將擴容閾值設置到 Integer.MAX_VALUE,并不代表不能裝新元素秕噪,只是數(shù)組長度將不會變化
       if (oldCap >= MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return oldTab;
       }//新容量擴大到原來的2倍钳降,擴容閾值也將擴大到原來閾值的兩倍
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
           newThr = oldThr << 1; // double threshold
   }
   //oldThr 不為空,代表我們使用帶參數(shù)的構(gòu)造方法指定了加載因子并計算了
   //初始初始閾值 會將擴容閾值 賦值給初始容量這里不再是期望容量腌巾,
   //但是 >= 指定的期望容量
   else if (oldThr > 0) // initial capacity was placed in threshold
       newCap = oldThr;
   else {
        // 空參數(shù)構(gòu)造會走這里初始化容量遂填,和擴容閾值 分別是 16 和 12
       newCap = DEFAULT_INITIAL_CAPACITY;
       newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
   }
   //如果新的擴容閾值是0,對應的是當前 table 為空澈蝙,但是有閾值的情況
   if (newThr == 0) {
        //計算新的擴容閾值
       float ft = (float)newCap * loadFactor;
       // 如果新的容量不大于 2^30 且 ft 不大于 2^30 的時候賦值給 newThr 
       //否則 使用 Integer.MAX_VALUE
       newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                 (int)ft : Integer.MAX_VALUE);
   }
   //更新全局擴容閾值
   threshold = newThr;
   @SuppressWarnings({"rawtypes","unchecked"})
    //使用新的容量創(chuàng)建新的哈希表的數(shù)組
   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
   table = newTab;
   //如果老的數(shù)組不為空將進行重新插入操作否則直接返回
   if (oldTab != null) {
        //遍歷老數(shù)組中每個位置的鏈表或者紅黑樹重新計算節(jié)點位置吓坚,插入新數(shù)組
       for (int j = 0; j < oldCap; ++j) {
           Node<K,V> e;//用來存儲對應數(shù)組位置鏈表頭節(jié)點
           //如果當前數(shù)組位置存在元素
           if ((e = oldTab[j]) != null) {
                // 釋放原來數(shù)組中的對應的空間
               oldTab[j] = null;
               // 如果鏈表只有一個節(jié)點,
               //則使用新的數(shù)組長度計算節(jié)點位于新數(shù)組中的角標并插入
               if (e.next == null)
                   newTab[e.hash & (newCap - 1)] = e;
               else if (e instanceof TreeNode)//如果當前節(jié)點為紅黑樹則需要進一步確定樹中節(jié)點位于新數(shù)組中的位置灯荧。
                   ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
               else { // preserve order
                   //因為擴容是容量翻倍礁击,
                   //原鏈表上的每個節(jié)點 現(xiàn)在可能存放在原來的下標,即low位逗载,
                   //或者擴容后的下標哆窿,即high位
              //低位鏈表的頭結(jié)點、尾節(jié)點
              Node<K,V> loHead = null, loTail = null;
              //高位鏈表的頭節(jié)點厉斟、尾節(jié)點
              Node<K,V> hiHead = null, hiTail = null;
              Node<K,V> next;//用來存放原鏈表中的節(jié)點
              do {
                  next = e.next;
                  // 利用哈希值 & 舊的容量挚躯,可以得到哈希值去模后,
                  //是大于等于 oldCap 還是小于 oldCap捏膨,
                  //等于 0 代表小于 oldCap秧均,應該存放在低位,
                  //否則存放在高位(稍后有圖片說明)
                  if ((e.hash & oldCap) == 0) {
                      //給頭尾節(jié)點指針賦值
                      if (loTail == null)
                          loHead = e;
                      else
                          loTail.next = e;
                      loTail = e;
                  }//高位也是相同的邏輯
                  else {
                      if (hiTail == null)
                          hiHead = e;
                      else
                          hiTail.next = e;
                      hiTail = e;
                  }//循環(huán)直到鏈表結(jié)束
              } while ((e = next) != null);
              //1.將低位鏈表存放在原index處号涯,
              if (loTail != null) {
                  loTail.next = null;
                  newTab[j] = loHead;
              }
              //2.將高位鏈表存放在新index處目胡,也就是原來index+原來的數(shù)組長度
              if (hiTail != null) {
                  hiTail.next = null;
                  newTab[j + oldCap] = hiHead;
              }
           }
       }
   }
   return newTab;
}

這一部分代碼量非常大,很多同學在這里迷失了链快,不過這里給大家寫了詳細的注釋誉己,可以幫助理解。這里其實分為2部分:

  • 1.設置擴容后的數(shù)組大小newCap和擴容后新的閾值newThr域蜗,都是擴大2倍巨双;
  • 2.擴容后原來數(shù)組數(shù)據(jù)的遷移,只有2種情況霉祸,要么原位置筑累,原來原位置+ oldCap

如果上面的代碼還沒有看懂,推薦一下這個視頻丝蹭,非常清晰
HashMap你不知道的小秘密

7慢宗、為什么加載因子為什么是 0.75?為什么樹化的條件是鏈表長度為8?為什么樹退化為鏈表長度為6镜沽?

簡單說下敏晤,為什么是 0.75而不是0.6,0.8缅茉。因為太小了會導致頻繁擴容resize嘴脾,數(shù)組空間利用率就不高;太大了的話蔬墩,雖然空間充分利用了译打,但是計算下標的時候沖突的概率就變大了。
別去分析了筹我,分析也意義不大扶平,這是大量數(shù)據(jù)計算后得出的一個在時間/空間上平衡(折衷)的方案帆离。

8蔬蕊、HashMap是否有序?

肯定不是啊哥谷,存放元素的時候是隨機的岸夯,所以無序。要有序的話们妥,可以選擇LinkedHashMap和TreeMap猜扮。建議面試的時候說一個就好,我喜歡說LinkedHashMap监婶。這個連環(huán)炮可以問出好多問題旅赢。
面試必備:LinkedHashMap源碼解析(JDK8)

9、HashMap是否線程安全惑惶?

線程不安全煮盼。多線程去put()的時候,有可能造成數(shù)據(jù)覆蓋带污,擴容的時候也可能會僵控。要做到線程安全,有這么一些方法:HashTable鱼冀、Collections.synchronizedMap()报破、ConcurrentHashMap。這里也是一個連環(huán)坑千绪,問這個問題的充易,一般希望你說一下ConcurrentHashMap原理,還會扯到多線程同步問題荸型,鎖機制盹靴,互斥鎖、自旋鎖、悲觀鎖鹉究、樂觀鎖宇立、等等。
ConcurrentHashMap基于JDK1.8源碼剖析

本文對你有所幫助自赔,點贊支持一下吧妈嘹!

參考

http://www.reibang.com/p/9ea8dd8dd40c

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绍妨,隨后出現(xiàn)的幾起案子润脸,更是在濱河造成了極大的恐慌,老刑警劉巖他去,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毙驯,死亡現(xiàn)場離奇詭異,居然都是意外死亡灾测,警方通過查閱死者的電腦和手機爆价,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來媳搪,“玉大人铭段,你說我怎么就攤上這事∏乇” “怎么了序愚?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長等限。 經(jīng)常有香客問我爸吮,道長,這世上最難降的妖魔是什么望门? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任形娇,我火速辦了婚禮,結(jié)果婚禮上怒允,老公的妹妹穿的比我還像新娘埂软。我一直安慰自己,他們只是感情好纫事,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布勘畔。 她就那樣靜靜地躺著,像睡著了一般丽惶。 火紅的嫁衣襯著肌膚如雪炫七。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天钾唬,我揣著相機與錄音万哪,去河邊找鬼侠驯。 笑死,一個胖子當著我的面吹牛奕巍,可吹牛的內(nèi)容都是我干的吟策。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼的止,長吁一口氣:“原來是場噩夢啊……” “哼檩坚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诅福,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤匾委,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后氓润,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赂乐,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年咖气,在試婚紗的時候發(fā)現(xiàn)自己被綠了挨措。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡采章,死狀恐怖运嗜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悯舟,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布砸民,位于F島的核電站抵怎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏岭参。R本人自食惡果不足惜反惕,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望演侯。 院中可真熱鬧姿染,春花似錦、人聲如沸秒际。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娄徊。三九已至闽颇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寄锐,已是汗流浹背兵多。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工尖啡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剩膘。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓衅斩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親怠褐。 傳聞我的和親對象是個殘疾皇子矛渴,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355