《吊打面試官》系列-HashMap

你知道的越多,你不知道的越多java

點贊再看,養成習慣git

本文 GitHub https://github.com/JavaFamily 上已經收錄,有一線大廠面試點思惟導圖,也整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。github

前言

做爲一個在互聯網公司面一次拿一次Offer的麪霸,戰勝了無數競爭對手,每次都只能看到無數落寞的身影失望的離開,略感愧疚(請容許我使用一下誇張的修辭手法)。面試

因而在一個寂寞難耐的夜晚,我痛定思痛,決定開始寫互聯網技術棧面試相關的文章,但願能幫助各位讀者之後面試勢如破竹,對面試官進行360°的反擊,吊打問你的面試官,讓一同面試的同僚瞠目結舌,瘋狂收割大廠Offer!算法

全部文章的名字只是個人噱頭,咱們應該有一顆謙遜的心,因此但願你們懷着空杯心態好好學,一塊兒進步。數組

正文

一個婀娜多姿,穿着襯衣的小姐姐,拿着一個精緻的小筆記本,徑直走過來坐在個人面前。安全

看着眼前這個美麗的女人,心想這不會就是Java基礎系列的面試官吧,真香。微信

不過看樣子這麼年輕應該問不出什麼深度的吧,嘻嘻。(哦?是麼😏)數據結構

小夥子,聽前面的面試官說了,你Redis和消息隊列都回答得不錯,看來仍是有點東西。多線程

美麗迷人的面試官您好,您見笑了,全靠看了敖丙的《吊打面試官》系列,否則我還真的回答不上不少本來的知識盲區,他真的有點東西。

面試官心想:哦,吊打面試官是麼,那今天我就讓你知道,吊打這兩個字怎麼寫的吧,年輕人啊,提早爲你感到可惜。

嗯嗯小帥比,雖然前面的技術棧沒啥太大的瑕疵,不過將來很長的一段時間我會用一期期的基礎教你作人的,你要準備好喲!

好了咱們開始今天的面試吧,小夥子你瞭解數據結構中的HashMap麼?能跟我聊聊他的結構和底層原理麼?

切,這也太看不起我了吧,竟然問這種低級問題,不過仍是要好好回答。

嗯嗯面試官,我知道HashMap是咱們很是經常使用的數據結構,由數組和鏈表組合構成的數據結構。

大概以下,數組裏面每一個地方都存了Key-Value這樣的實例,在Java7叫Entry在Java8中叫Node。

由於他自己全部的位置都爲null,在put插入的時候會根據key的hash去計算一個index值。

就好比我put(」帥丙「,520),我插入了爲」帥丙「的元素,這個時候咱們會經過哈希函數計算出插入的位置,計算出來index是2那結果以下。

hash(「帥丙」)= 2

你提到了還有鏈表,爲啥須要鏈表,鏈表又是怎麼樣子的呢?

咱們都知道數組長度是有限的,在有限的長度裏面咱們使用哈希,哈希自己就存在機率性,就是」帥丙「和」丙帥「咱們都去hash有必定的機率會同樣,就像上面的狀況我再次哈希」丙帥「極端狀況也會hash到一個值上,那就造成了鏈表。

每個節點都會保存自身的hash、key、value、以及下個節點,我看看Node的源碼。

說到鏈表我想問一下,你知道新的Entry節點在插入鏈表的時候,是怎麼插入的麼?

java8以前是頭插法,就是說新來的值會取代原有的值,原有的值就順推到鏈表中去,就像上面的例子同樣,由於寫這個代碼的做者認爲後來的值被查找的可能性更大一點,提高查找的效率。

可是,在java8以後,都是所用尾部插入了。

爲啥改成尾部插入呢?

這!!!這個問題,面試官可真會問!!!還好我飽讀詩書,否則死定了!

有人認爲是做者隨性而爲,沒啥luan用,其實否則,其中暗藏玄機

首先咱們看下HashMap的擴容機制:

帥丙提到過了,數組容量是有限的,數據屢次插入的,到達必定的數量就會進行擴容,也就是resize。

何時resize呢?

有兩個因素:

  • Capacity:HashMap當前長度。
  • LoadFactor:負載因子,默認值0.75f。

怎麼理解呢,就好比當前的容量大小爲100,當你存進第76個的時候,判斷髮現須要進行resize了,那就進行擴容,可是HashMap的擴容也不是簡單的擴大點容量這麼簡單的。

擴容?它是怎麼擴容的呢?

分爲兩步

  • 擴容:建立一個新的Entry空數組,長度是原數組的2倍。
  • ReHash:遍歷原Entry數組,把全部的Entry從新Hash到新數組。

爲何要從新Hash呢,直接複製過去不香麼?

臥槽這個問題!有點知識盲區呀!

1x1得 1 1x2 得 2 …. 有了,我想起來敖丙那天晚上在我耳邊的話了:假如我年少有爲不自卑,懂得什麼是珍貴,那些好夢沒給你,我一輩子有愧….什麼鬼!

小姐姐:是由於長度擴大之後,Hash的規則也隨之改變。

Hash的公式---> index = HashCode(Key) & (Length - 1)

原來長度(Length)是8你位運算出來的值是2 ,新的長度是16你位運算出來的值明顯不同了。

擴容前:

擴容後:

說完擴容機制咱們言歸正傳,爲啥以前用頭插法,java8以後改爲尾插了呢?

臥槽,我覺得她忘記了!竟然仍是被問到了!

我先舉個例子吧,咱們如今往一個容量大小爲2的put兩個值,負載因子是0.75是否是咱們在put第二個的時候就會進行resize?

2*0.75 = 1 因此插入第二個就要resize了

如今咱們要在容量爲2的容器裏面用不一樣線程插入A,B,C,假如咱們在resize以前打個短點,那意味着數據都插入了可是還沒resize那擴容前多是這樣的。

咱們能夠看到鏈表的指向A->B->C

Tip:A的下一個指針是指向B的

由於resize的賦值方式,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,經過從新計算索引位置後,有可能被放到了新數組的不一樣位置上。

就可能出現下面的狀況,你們發現問題沒有?

B的下一個指針指向了A

一旦幾個線程都調整完成,就可能出現環形鏈表

若是這個時候去取值,悲劇就出現了——Infinite Loop。

誒臥槽,小夥子難不倒他呀!

小夥子有點東西呀,可是你都都說了頭插是JDK1.7的那1.8的尾插是怎麼樣的呢?

由於java8以後鏈表有紅黑樹的部分,你們能夠看到代碼已經多了不少if else的邏輯判斷了,紅黑樹的引入巧妙的將本來O(n)的時間複雜度下降到了O(logn)。

Tip:紅黑樹的知識點一樣很重要,仍是那句話不打沒把握的仗,限於篇幅緣由,我就不在這裏過多描述了,之後寫到數據結構再說吧,不過要面試的仔,仍是要準備好,反正我是常常問到的。

使用頭插會改變鏈表的上的順序,可是若是使用尾插,在擴容時會保持鏈表元素本來的順序,就不會出現鏈表成環的問題了。

就是說本來是A->B,在擴容後那個鏈表仍是A->B

Java7在多線程操做HashMap時可能引發死循環,緣由是擴容轉移後先後鏈表順序倒置,在轉移過程當中修改了原來鏈表中節點的引用關係。

Java8在一樣的前提下並不會引發死循環,緣由是擴容轉移後先後鏈表順序不變,保持以前節點的引用關係。

那是否是意味着Java8就能夠把HashMap用在多線程中呢?

我認爲即便不會出現死循環,可是經過源碼看到put/get方法都沒有加同步鎖,多線程狀況最容易出現的就是:沒法保證上一秒put的值,下一秒get的時候仍是原值,因此線程安全仍是沒法保證。

小夥子回答得很好嘛,這都被你回答道了,面試這麼多人都不知道頭插和尾插,仍是被你說出來了,能夠能夠。

面試官謬讚啊,要不是你這樣美若天仙的面試官面試我,我估計是想不起來了。

我*,你套近乎?

小姐姐抿嘴一笑,小子你offer有了,耶穌都帶不走你,我說的!

那我問你HashMap的默認初始化長度是多少?

我記得我在看源碼的時候初始化大小是16

你那知道爲啥是16麼?

臥*,這叫什麼問題啊?他爲啥是16我怎麼知道???你肯定你沒逗我?

我努力回憶源碼,不知道有沒有漏掉什麼細節,之前在學校熬夜看源碼的一幕幕在腦海裏閃過,想起那個晚上在操場上,跟我好了半個月的小綠拉着個人手說:你就要當爸爸了。

等等,這都是什麼鬼,哦哦哦,想起來了!!!

在JDK1.8的 236 行有1<<4就是16,爲啥用位運算呢?直接寫16很差麼?

我再次陷入沉思,瘋狂腦暴,叮!

有了!

面試官您好,咱們在建立HashMap的時候,阿里巴巴規範插件會提醒咱們最好賦初值,並且最好是2的冪。

這樣是爲了位運算的方便,位與運算比算數計算的效率高了不少,之因此選擇16,是爲了服務將Key映射到index的算法。

我前面說了全部的key咱們都會拿到他的hash,可是咱們怎麼儘量的獲得一個均勻分佈的hash呢?

是的咱們經過Key的HashCode值去作位運算。

我打個比方,key爲」帥丙「的十進制爲766132那二進制就是 10111011000010110100

咱們再看下index的計算公式:index = HashCode(Key) & (Length- 1)

15的的二進制是1111,那10111011000010110100 &1111 十進制就是4

之因此用位與運算效果與取模同樣,性能也提升了很多!

那爲啥用16不用別的呢?

由於在使用不是2的冪的數字的時候,Length-1的值是全部二進制位全爲1,這種狀況下,index的結果等同於HashCode後幾位的值。

只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。

這是爲了實現均勻分佈

喲小傢伙,知道的確實不少,那我問你個問題,爲啥咱們重寫equals方法的時候須要重寫hashCode方法呢?

你能用HashMap給我舉個例子麼?

這都能被他問到,還好我看了敖丙的系列呀,否則真的完了!!!

可是我想拖延點時間,只能故作沉思,仰望天空片刻,45°仰望天空的樣子,說實話,我看到面試官都流口水了!惋惜我是他永遠得不到的男人,好了不裝逼了。

我想起來了面試官!

由於在java中,全部的對象都是繼承於Object類。Ojbect類中有兩個方法equals、hashCode,這兩個方法都是用來比較兩個對象是否相等的。

在未重寫equals方法咱們是繼承了object的equals方法,那裏的 equals是比較兩個對象的內存地址,顯然咱們new了2個對象內存地址確定不同

  • 對於值對象,==比較的是兩個對象的值
  • 對於引用對象,比較的是兩個對象的地址

你們是否還記得我說的HashMap是經過key的hashCode去尋找index的,那index同樣就造成鏈表了,也就是說」帥丙「和」丙帥「的index均可能是2,在一個鏈表上的。

咱們去get的時候,他就是根據key去hash而後計算出index,找到了2,那我怎麼找到具體的」帥丙「仍是」丙帥「呢?

equals!是的,因此若是咱們對equals方法進行了重寫,建議必定要對hashCode方法重寫,以保證相同的對象返回相同的hash值,不一樣的對象返回不一樣的hash值。

否則一個鏈表的對象,你哪裏知道你要找的是哪一個,到時候發現hashCode都同樣,這不是完犢子嘛。

能夠能夠小夥子,我記得你上面說過他是線程不安全的,那你能跟我聊聊大家是怎麼處理HashMap在線程安全的場景麼?

面試官,在這樣的場景,咱們通常都會使用HashTable或者ConcurrentHashMap,可是由於前者的併發度的緣由基本上沒啥使用場景了,因此存在線程不安全的場景咱們都使用的是ConcurrentHashMap。

HashTable我看過他的源碼,很簡單粗暴,直接在方法上鎖,併發度很低,最多同時容許一個線程訪問,ConcurrentHashMap就好不少了,1.7和1.8有較大的不一樣,不過併發度都比前者好太多了。

那你能跟我聊聊ConcurrentHashMap麼?

好呀,不過今每天色已晚,我以爲咱們要不改天再約?

再說最近敖丙好像雙十二比較忙,一次怎麼能懟這麼多呢?

好吧好吧,小夥子還挺會爲別人着想,並且還喜歡這麼優秀的做者,你我以爲來日可期,那咱們改日再約,今天表現很好,但願下次能保持住!

總結

HashMap絕對是最常問的集合之一,基本上全部點都要爛熟於心的那種,篇幅和時間的關係,我就很少介紹了,核心的點我基本上都講到了,不過像紅黑樹這樣的就沒怎麼聊了,可是不表明不重要。

篇幅和精力的緣由我就介紹到了一部分的主要知識點,我總結了一些關於HashMap常見的面試題,你們問下本身能不能回答上來,不能的話要去查清楚喲。

HashMap常見面試題:

  • HashMap的底層數據結構?

  • HashMap的存取原理?

  • Java7和Java8的區別?

  • 爲啥會線程不安全?

  • 有什麼線程安全的類代替麼?

  • 默認初始化大小是多少?爲啥是這麼多?爲啥大小都是2的冪?

  • HashMap的擴容方式?負載因子是多少?爲什是這麼多?

  • HashMap的主要參數都有哪些?

  • HashMap是怎麼處理hash碰撞的?

  • hash的計算規則?

點關注,不迷路

好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才

我後面會每週都更新幾篇一線互聯網大廠面試和經常使用技術棧相關的文章,很是感謝人才們能看到這裏,若是這個文章寫得還不錯,以爲「敖丙」我有點東西的話 求點贊👍 求關注❤️ 求分享👥 對暖男我來講真的 很是有用!!!

白嫖很差,創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!

敖丙 | 文 【原創】

若是本篇博客有任何錯誤,請批評指教,不勝感激 !


文章每週持續更新,能夠微信搜索「 三太子敖丙 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/JavaFamily 已經收錄,有一線大廠面試點思惟導圖,也整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。

相關文章
相關標籤/搜索