HashMap面試題

菜鳥的突變發表於2020-11-04

HashMap面試題

面試題:

  1. JDK8中的HashMap有哪些改動?
  2. HashMap的工作原理嗎?
  3. HashMap中的“死鎖”是怎麼回事?
  4. HashMap的擴容機制是怎麼樣的?JDK7與JDK8有什麼不同嗎?
  5. HashMap是執行緒安全的嗎?為什麼?
  6. HashMap新增的物件為什麼要重寫equals和hashcode?
  7. JDK8中為什麼要使用紅黑樹?

JDK8中的HashMap有哪些改動

JDK7-HashMap是用位桶+連結串列的形式
JDK8-HashMap是用位桶+連結串列/紅黑樹的形式 (衝突節點數不小於8-1時,轉換成紅黑樹。)
深處的還需要去了解下面這些?
1:什麼是桶排序?
2:紅黑樹
3:get是如何獲取資料
4:擴容機制resize()

HashMap的工作原理嗎?

HashMap的工作原理 :HashMap是基於雜湊法(又稱雜湊法)的原理,使用put(key, value)儲存物件到HashMap中,使用get(key)從HashMap中獲取物件。當我們給put()方法傳遞鍵和值時,我們先對鍵呼叫hashCode()方法,返回的hashCode用於找到bucket(桶)位置來儲存Entry物件。HashMap是在bucket中儲存鍵物件和值物件,作為Map.Entry。並不是僅僅只在bucket中儲存值

HashMap中的“死鎖”是怎麼回事?

HashMap會造成死鎖,因為HashMap是執行緒非安全的,多併發的情況容易造成死鎖,若要高併發推薦使用ConcurrentHashMap;
二個程式T1、T2,HashMap容量為2,T1執行緒放入key A、B、C、D、E。在T1執行緒中A、B、C Hash值相同,於是形成一個連結,假設為A->C->B,而D、E Hash值不同,於是容量不足,需要新建一個更大尺寸的hash表,然後把資料從老的Hash表中
遷移到新的Hash表中(refresh)。這時T2程式闖進來了,T1暫時掛起,T2程式也準備放入新的key,這時也
發現容量不足,也refresh一把。refresh之後原來的連結串列結構假設為C->A,之後T1程式繼續執行,連結結構
為A->C,這時就形成A.next=B,B.next=A的環形連結串列。一旦取值進入這個環形連結串列就會陷入死迴圈。
產生死鎖的根本原因就是在高併發條件下,hashmap擴容的時候產生頭尾相連出現死迴圈而導致的。
解決辦法
使用ConcurrentHashMap進行替代hash,利用ConcurrentHashMap的執行緒安全,加鎖(sgement)的方式,內部的結構可以讓其在進行寫操作的時候能夠將鎖的粒度

HashMap的擴容機制是怎麼樣的?JDK7與JDK8有什麼不同嗎?

HashMap擴容:
JDK7的時候,Hashmap擴容的時候,當有新的元素進來的時候,他不僅僅會判斷是否大於閾值,還會看當前的陣列位置是否為空,就算這時候已經大於閾值了,但是當前陣列位置為空的時候,他也不會擴容,陣列擴容只有一個辦法:只能把元素存入一個新的陣列。
JDK1.8中在計算新位置的時候並沒有跟1.7中一樣重新進行hash運算,而是用了原位置+原陣列長度這樣一種很巧妙的方式,而這個結果與hash運算得到的結果是一致的,只是會更塊。rehash之後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。

JDK1.7 HashMap的結構: Entry陣列+連結串列
JDK1.8 HashMap的結構: Node陣列+連結串列+紅黑樹
儲存原理
根據上面圖片, 我們可以看出, 如果 HashMap 的每個 bucket 裡只有一個 Entry 時,HashMap 可以根據索引、快速地取出該 bucket 裡的 Entry;在發生“Hash 衝突”的情況下,單個 bucket 裡儲存的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜尋的 Entry 為止——如果恰好要搜尋的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須迴圈到最後才能找到該元素。
負載因子和擴容
initailCapacity * loadFactor = HashMap容量
當建立 HashMap 時,有一個預設的負載因子(load factor),其預設值為 0.75,這是時間和空間成本上一種折衷:增大負載因子可以減少 Hash 表(就是那個 Entry 陣列)所佔用的記憶體空間,但會增加查詢資料的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);減小負載因子會提高資料查詢的效能,但會增加 Hash 表所佔用的記憶體空間。

如果開始就知道 HashMap 會儲存多個 key-value 對,可以在建立時就使用較大的初始化容量,如果 HashMap 中 Entry 的數量一直不會超過極限容量(capacity * load factor),HashMap 就無需呼叫 resize() 方法重新分配 table 陣列,從而保證較好的效能。

HashMap的大小很簡單,不是實時計算的,而是每次新增加Entry/Node的時候,size就遞增。刪除的時候就遞減。當到達閾值(負載因子*總size)時, 容量翻倍, 並且永遠是2^n, 因為hash取模運算太慢, 2^n的容量可以進行位運算

jdk 1.7 頭插
jdk 1.8+ 尾插

因為hashmap在併發resize時會出現的死迴圈問題, 並且1.7時候用頭插是考慮到了一個所謂的熱點資料的點(新插入的資料可能會更早用到),但這其實是個偽命題, 因為JDK1.7中rehash的時候,舊連結串列遷移新連結串列的時候,如果在新表的陣列索引位置相同,則連結串列元素會倒置(就是因為頭插) 所以最後的結果 還是打亂了插入的順序 所以總的來看支撐1.7使用頭插的這點原因也不足以支撐下去了 所以就乾脆換成尾插 一舉多得

HashMap是執行緒安全的嗎?為什麼?

答:不安全
原因:hashmap在高併發的情況下執行擴容會出現死鎖的問題。
面試官還會問:產生死鎖的原因是什麼?如何解決?採用這種方式將會帶來哪些問題?
死鎖:多執行緒下擴容的時候產生死迴圈,採用ConcurrentHashMap來代替hashmap,帶來的問題是ConcurrentHashMap的效率低,

HashMap新增的物件為什麼要重寫equals和hashcode

在我們的業務系統中判斷物件時有時候需要的不是一種嚴格意義上的相等,而是一種業務上的物件相等。在這種情況下,原生的equals方法就不能滿足我們的需求了
所以這個時候我們需要重寫equals方法,來滿足我們的業務系統上的需求。那麼為什麼在重寫equals方法的時候需要重寫hashCode方法呢?
Object.hashCode的通用約定:
1:在一個應用程式執行期間,如果一個物件的equals方法做比較所用到的資訊沒有被修改的話,那麼,對該物件呼叫hashCode方法多次,它必須始終如一地返回 同一個整數。在同一個應用程式的多次執行過程中,這個整數可以不同,即這個應用程式這次執行返回的整數與下一次執行返回的整數可以不一致。
2:如果兩個物件根據equals(Object)方法是相等的,那麼呼叫這兩個物件中任一個物件的hashCode方法必須產生同樣的整數結果。
3:如果兩個物件根據equals(Object)方法是不相等的,那麼呼叫這兩個物件中任一個物件的hashCode方法,不要求必須產生不同的整數結果。然而,程式設計師應該意識到這樣的事實,對於不相等的物件產生截然不同的整數結果,有可能提高雜湊表(hash table)的效能。

HashSet和HashMap這些基於雜湊值(hash)實現的類。HashMap的底層處理機制是以陣列的方法儲存放入的資料的(Node<K,V>[] table),其中的關鍵是陣列下標的處理。陣列的下標是根據傳入的元素hashCode方法的返回值再和特定的值異或決定的。如果該陣列位置上已經有放入的值了,且傳入的鍵值相等則不處理,若不相等則覆蓋原來的值,如果陣列位置沒有條目,則插入,並加入到相應的連結串列中。檢查鍵是否存在也是根據hashCode值來確定的。所以如果不重寫hashCode的話,可能導致HashSet、HashMap不能正常的運作、

如果我們將某個自定義物件存到HashMap或者HashSet及其類似實現類中的時候,如果該物件的屬性參與了hashCode的計算,那麼就不能修改該物件引數hashCode計算的屬性了。有可能會移除不了元素,導致記憶體洩漏。
總結:
1.new Object(),JVM根據這個物件的Hashcode值,放入到對應的Hash表對應的Key上,如果不同的物件確產生了相同的hash值,也就是發生了Hash key相同導致衝突的情況,那麼就在這個Hash key的地方產生一個連結串列,將所有產生相同hashcode的物件放到這個單連結串列上去,串在一起。

2.比較兩個物件的時候,首先根據他們的hashcode去hash表中找他的物件,當兩個物件的hashcode相同,那麼就是說他們這兩個物件放在Hash表中的同一個key上,那麼他們一定在這個key上的連結串列上。那麼此時就只能根據Object的equal方法來比較這個物件是否equal。當兩個物件的hashcode不同的話,肯定他們不能equals.

JDK8中為什麼要使用紅黑樹

原因:jdk1.8版本後,Java對HashMap做了改進,在連結串列長度大於8的時候,將後面的資料存在紅黑樹中,以加快檢索速度
紅黑樹的特徵:
1:每個節點或者是黑色,或者是紅色。
2:根節點是黑色。
3:每個葉子節點(NIL)是黑色。 [注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]
4:如果一個節點是紅色的,則它的子節點必須是黑色的。
5:從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
紅黑樹的優勢
紅黑樹相比avl樹,在檢索的時候效率其實差不多,都是通過平衡來二分查詢。但對於插入刪除等操作效率提高很多。紅黑樹不像avl樹一樣追求絕對的平衡,他允許區域性很少的不完全平衡,這樣對於效率影響不大,但省去了很多沒有必要的調平衡操作,avl樹調平衡有時候代價較大,所以效率不如紅黑樹,在現在很多地方都是底層都是紅黑樹的天下啦。

紅黑樹的高度只比高度平衡的AVL樹的高度(log2n)僅僅大了一倍,在效能上卻好很多。
總結:
java8不是用紅黑樹來管理hashmap,而是在hash值相同的情況下(且重複數量大於8),用紅黑樹來管理資料。 紅黑樹相當於排序資料,可以自動的使用二分法進行定位,效能較高。一般情況下,hash值做的比較好的話基本上用不到紅黑樹。

紅黑樹犧牲了一些查詢效能 但其本身並不是完全平衡的二叉樹。因此插入刪除操作效率略高於AVL樹。
AVL樹用於自平衡的計算犧牲了插入刪除效能,但是因為最多隻有一層的高度差,查詢效率會高一些。

相關文章