帶你一起擼HashMap! ! ! !! ! ! !六到飛起的那種! ! ! !

面試中大家常被問到HashMap的相關知識點,聽說是因爲HashMap裏面有很多知識點可以體現一爲程序員基本功扎不紮實.

什麼是HashMap?( hashmap的初始值是16,即2的4次方,之後的每次擴容都是兩倍擴容)

HashMap基於Map接口實現,元素以鍵值對的方式存儲,並且允許使用null 鍵和null值,因爲key不允許重複,因此只能有一個鍵爲null,另外HashMap不能保證放入元素的順序,它是無序的。HashMap是線程不安全的。

HashMap 結構示意圖

 

從下圖中可以看出HashMap的PUT方法的詳細過程.其中造成線程不安全的方法主要是resize(擴容)方法.

問題一:HashMap不安全原因:

(1)在put的時候,因爲該方法不是同步的,假如有兩個線程A,B它們的put的key的hash值相同,不論是從頭插入還是從尾插入,假如A獲取了插入位置爲x,但是還未插入,此時B也計算出待插入位置爲x,則不論AB插入的先後順序肯定有一個會丟失

(2)在擴容的時候,jdk1.8之前是採用頭插法,當兩個線程同時檢測到hashmap需要擴容,在進行同時擴容的時候有可能會造成鏈表的循環,主要原因就是,採用頭插法,新鏈表與舊鏈表的順序是反的,在1.8後採用尾插法就不會出現這種問題,同時1.8的鏈表長度如果大於8就會轉變成紅黑樹。

問題二:HashMap不安全的體現?

  1. 首先HashMap在jdk1.7中的問題,相信大家都知道在jdk1.7多線程環境下HashMap容易出現死循環,而且當併發執行擴容操作時會造成環形鏈和數據丟失的情況。
  2. 在JDK1.8中,在併發執行put操作時會發生數據覆蓋的情況。

 

問題三:如何讓HashMap變成線程安全的?

  1. 替換成Hashtable,hashtable底層是數組+鏈表的形式,其中的get、put方法等都是用synchronized修飾的,因此,hashtable是線程安全的,但是執行效率比較低,一般不推薦使用。

         原因:使用synchronize來保證線程安全,即當有一個線程擁有鎖的時候,其他的線程都會進入阻塞或者輪詢狀態,這樣會使得效率越來越低

     2. 使用Collections類的synchronizedMap方法包裝一下,獲得線程安全的HashMap。(synchronizedMap的線程安全和hashtable一樣,都是使用sunchronized方法,將方法上鎖。)方法如下:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)  返回由指定映射支持的同步(線程安全的)映射

//或者
Map map = Collections.synchronizedMap(new HashMap());

     3. 使用ConcurrentHashMap,它使用分段鎖來保證線程安全並且可以有效提高併發訪問率, 這個方法效率比較高,推薦使用.

原因:

HashTable訪問效率低下的原因,就是因爲所有的線程在競爭同一把鎖.如果容器中有多把鎖,不同的鎖鎖定不同的位置,這樣線程間就不會存在鎖的競爭,這樣就可以有效的提高併發訪問效率,這就是currentHashMap所使用的鎖分段技術
將數據一段一段的存儲,然後爲每一段都配一把鎖,當一個線程只是佔用其中的一個數據段時,其他段的數據也能被其他線程訪問,這樣的話,當修改該容器的不同的段時,就不會存在併發的問題.

通過前兩種方式獲得線程安全的HashMap在讀寫數據的時候會對整個容器上鎖,而ConcurrentHashMap並不需要對整個容器上鎖,它只需要鎖住要修改的部分就行了.另外當一個新節點想要插入hashmap的鏈表時,在jdk1.8之前的版本是插在頭部,在1.8後是插在尾部.

問題四:說一下ConcurrentHashMap吧?

問題五:HashMap和HashTable的區別?

  1. HashMap和Hashtable都實現了Map接口,因此很多特性非常相似。但是,他們有以下不同點:
  2. HashMap允許鍵和值是null,而Hashtable不允許鍵或者值是null。
  3. Hashtable是同步的,而HashMap不是。因此,HashMap更適合於單線程環境,而Hashtable適合於多線程環境。
  4. HashMap提供了可供應用迭代的鍵的集合,因此,HashMap是快速失敗的。另一方面,Hashtable提供了對鍵的列舉(Enumeration)。
  5. 一般認爲Hashtable是一個遺留的類。

問題六:HashMap和HashSet的區別

問題七:HashMap的工作原理

HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然後返回值對象。HashMap使用LinkedList來解決碰撞問題,當發生碰撞了,對象將會儲存在LinkedList的下一個節點中。 HashMap在每個LinkedList節點中儲存鍵值對對象。

當兩個不同的鍵對象的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的LinkedList中。鍵對象的equals()方法用來找到鍵值對。

問題八:HashMap存儲結構(HashMap不是有序的)

這裏需要區分一下,JDK1.7和 JDK1.8之後的 HashMap 存儲結構。在JDK1.7及之前,是用數組加鏈表的方式存儲的。這裏先分析在jdk1.7中的問題,相信大家都知道在jdk1.7多線程環境下HashMap容易出現死循環, HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

  1. 數組:存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,爲O(1);數組的特點是:尋址容易,插入和刪除困難;
  2. 鏈表:存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。
  3. HashMap:哈希表((Hash table)既滿足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。採用Entry數組來存儲key-value對,每一個鍵值對組成了一個Entry實體,Entry類實際上是一個單向的鏈表結構,它具有Next指針,可以連接下一個Entry實體,以此來解決Hash衝突的問題。

問題九:爲什麼要引入紅黑樹?

JDK 1.8以前是數組+鏈表,還未引入紅黑樹,這就導致了鏈表過長時查找的時間複雜度是O(n), 完全失去了設計HashMap時的設計初衷,針對這種情況JDK 1.8引入了紅黑樹(查找的時間複雜度爲O(logN),什麼是紅黑樹呢,紅黑樹是一種自平衡的二叉查找樹,不是一種絕對平衡的二叉樹,它放棄了追求絕對平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多只需要三次旋轉就能達到平衡,實現起來也更爲簡單。從而獲得更高的查找性能。

問題十:hash衝突有哪些解決辦法?

答:hashMap採用拉鍊法來解決hash衝突,將哈希地址相同的對象加到同一鏈表中,JDK1.8之前採用的是頭插法,1.8之後採用的是尾插法。(還有 開放定址法,再哈希法)

問題十一:爲什麼HashMap不用LinkedList,而選用數組?

答:因爲數組的查找效率更高。在hashMap中,定位桶的位置是利用元素的key的哈希值對數組長度取模得到,然後數組通過計算所得到的下標值可以迅速找到桶的位置。

問題十二:爲什麼HashMap不用ArrayList?

答:hashMap的數組擴容是2的n次方,做取模運算的效率高,而ArrayList的擴容機制是1.5倍的擴容。

問題十三:什麼是加載因子 loadFactor?

答:表示數組的稀疏程度,越大,數組存放的 entry 就越多,也就越稠密,太大會導致查找效率變低;越小,數組存放的 entry 就越少,也就越稀疏,太小會導致數組利用率低。

問題十四:HashMap在什麼條件下擴容?及擴容過程

答:

  • 當數組存放元素的個數超過了加載因子與當前容量的乘積,就需要進行擴容。其中加載因子爲0.75f,它是時間和空間的一個折中值。
  • 擴容過程:首先創建一個新的空數組,長度是原數組的兩倍。然後遍歷原數組,把所有元素重新 Hash 到新數組。之所以重新 hash 而不是直接複製進去,是因爲長度擴大後,hash 的規則就變了,hash 的公式是 index = hash & (length -1) ,可以看出公式中的 length 值變化了,所以需要重新 hash。

問題十五:爲什麼在解決hash衝突時,不直接用紅黑樹? 而選擇先用鏈表,再轉紅黑樹?

答:因爲紅黑樹需要進行左旋、右旋、變色這些操作來保持平衡,而單鏈表不需要。當元素個數小於8時,此時做查詢操作,鏈表結構具有更好的性能。當元素個數大於8時,此時需要紅黑樹來加快查詢速度。如果一開始就用紅黑樹結構,元素太少,增刪效率又比較慢,於是浪費了性能。

問題十六:不用紅黑樹,用二叉查找樹可以麼?

答:可以是可以,但是二叉查找樹在特殊情況下會變成一條線性結構,比如只有左子樹,這就跟原來使用的鏈表結構一樣了,遍歷查找會很慢。

問題十七:爲什麼閥值是8?

答:因爲泊松分佈,在負載因子默認爲0.75時,單個hash槽內元素個數爲8的概率小於百萬分之一,所以將7作爲一個分水嶺,等於7時不轉換,大於等於8時才進行轉換,小於等於6時就退化爲鏈表。

問題十八:你一般用什麼作爲HashMap的key? 爲什麼 String、Integer 適合作爲鍵?
答:一般用Integer、String這種不可變類作爲 HashMap 的key,String最爲常用。原因有兩點:
(1) 因爲字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算,這就使得字符串很適合作爲Map中的鍵。
(2) 因爲獲取對象的時候要用到equals()和hashCode()方法,那麼重寫這兩個方法是非常重要的,而Integer和String這些類已經很規範的重寫了hashCode()以及equals()方法。


問題十九:用可變類當HashMap的key有什麼問題?
答:hashcode可能發生改變,導致put進去的值,無法get出來。

問題二十:爲什麼重寫equals方法後必須重寫hashCode方法
答:HashMap 通過 key 的 hashCode 來找到桶的位置,如果兩個數據計算出桶的位置一樣,它們就會形成鏈表存放在同一個桶中。重寫 hashCode 就是爲了保證相同的對象返回相同的 hash 值,不同的對象返回不同的 hash 值。hashCode 相同時值未必相同,所以還需要通過重寫 equals 的方法來比較兩元素的值是否相同。

問題二十一:Java中爲什麼重寫 equals 方法必須重寫 hashCode 方法?

答:因爲 Java 規定,如果兩個對象 equals 的結果爲 true,那麼 hashCode 也必須相等。

問題二十二:如果HashMap的大小超過了負載因子(load factor)定義的容量, 該怎麼辦 ?

答:默認的負載因子是0.75, 也就是說, 當一個map填滿了75%的bucket時, 和其他集合類一樣, 就會創建原來HashMap大小的兩倍的bucket數組, 來重新調整map的大小, 並將原來的對象放入新的bucket數組中, 這個過程叫rehashing, 因爲他調用hash方法找到新的bucket位置.
 

問題二十三:如果兩個鍵的hashCode相同, 你如何獲取值對象?

答:當我們調用get() 方法, HashMap會使用鍵對象的hashCode找到bucket位置, 然後獲取值對象. 如果有兩個值對象存儲在同一個bucket, 將會遍歷LinkedList知道找到值對象, 找到bucket位置之後, 會調用keys.equals() 方法找到LinkedList中正確的結點, 最終找到要找的值對象. ( 當程序通過key取到對應的value時, 系統只要先計算出該key的hashCode() 返回值, 在根據該hashCode返回值找出該key在table數組中的索引, 然後取出該索引除的Entry, 最後返回該key對應的value即可)
 

問題二十四:當兩個對象的hashCode相同會發生什麼?

答:因爲hashCode相同, 所以他們的bucket位置相同, 碰撞會發生, 因爲HashMap使用鏈表存儲對象, 這個Entry(包含有鍵值對的Map.Entry)會存儲在鏈表中, 這個時候要根據hashCode來劃分數組, 如果數組的座標相同, 則進入鏈表這個數據結構中了, 一般的添加都在最前面, 也就是和數組下標直接相連的地方, 鏈表長度到達8的時候, JDK1.8上升爲紅黑樹. 這樣回答基本就沒問題了.

問題二十五:JDK8中的HashMap有哪些改進?

答:

  • JDK7中的底層實現是數組+鏈表,JDK8中使用的是數組+鏈表+紅黑樹
  • JDK7中擴容時可能出現死鎖,JDK8中通過算法優化不會出現死鎖
  • JDK8中對哈希值的哈希算法進行了簡化以提高算法效率

問題二十六:在使用HashMap的時候應該注意啥?

答:

  • HashMap的擴容機制是很影響效率的,所以如果事先能確定有多少個元素需要存儲,那麼建議在初始化HashMap時對數組的容量也進行初始化,防止擴容
  • HashMap中使用了對象的hashcode方法,而且很關鍵,所以再重寫對象的equals時建議一定要重寫hashcode方法
  • 如果是用對象作爲HashMap的Key,那麼將對象設置爲final,以防止對象被重新賦值,因爲一旦重新賦值其實就代表了一個新對象作爲key,因爲兩個對象的hashcode可能不同
     

問題二十七:HashMap初始容量是多少?擴容容量是多少?怎麼擴容?

答:初始容量爲16,擴容後爲舊容量的兩倍

擴容:創建一個新的Entry空數組,長度是原數組的2倍。2.ReHash:遍歷原Entry數組,把所有的Entry重新Hash到新數組。

問題二十八:遍歷HashMap的方法分別是?

答:共有三種方法

28.1

//1、keySet遍歷
for(String key : map.keySet()){
    System.out.println(map.get(key));
}

28.2

// 遍歷enrtySet
Set<Map.Entry<String,Object>> entrySet = map.entrySet(); 
for(Map.Entry<String,Object> entry : entrySet){
     System.out.println(entry.getKey()+"-"+entry.getValue());
}

28.3

 // 利用迭代器,因爲entrySet本質上是個Set集合
 Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();
 while(iterator.hasNext()){
     Map.Entry<String,Object> mapEntry = iterator.next();
     System.out.println(mapEntry.getKey()+"-"+mapEntry.getValue());
 }

問題二十九:我們可以使用CocurrentHashMap來代替Hashtable嗎?

答:我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因爲它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。它們都可以用於多線程的環境,但是當Hashtable的大小增加到一定的時候,性能會急劇下降,因爲迭代時需要被鎖定很長的時間。因爲ConcurrentHashMap引入了分割(segmentation)(分段鎖),不論它變得多麼大,僅僅需要鎖定map的某個部分,而其它的線程不需要等到迭代完成才能訪問map。簡而言之,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。

問題三十:什麼是哈希衝突(哈希碰撞)?

答:鍵(key)經過hash函數得到的結果作爲地址去存放當前的鍵值對(key-value)(hashmap的存值方式),但是卻發現該地址已經有值了,就會產生衝突。這個衝突就是hash衝突。(如果兩個不同對象的hashCode相同,這種現象稱爲hash衝突)

 

-------------------------------------------------------------寫的不好,僅供參考----------------------------------------------------------------------------

 

 

 

 

 

(“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”

你也許會回答“我沒有詳查標準的Java API,你可以看看Java源代碼或者Open JDK。”“我可以用Google找到答案。”

但一些面試者可能可以給出答案,“HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。”這裏關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,作爲Map.Entry。這一點有助於理解獲取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認爲僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案相當的正確,也顯示出面試者確實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當面試官加入一些Java程序員每天要碰到的實際場景的時候,錯誤的答案頻現。下個問題可能是關於HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:

“當兩個對象的hashcode相同會發生什麼?” 從這裏開始,真正的困惑開始了,一些面試者會回答因爲hashcode相同,所以兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。然後面試官可能會提醒他們有equals()和hashCode()兩個方法,並告訴他們兩個對象就算hashcode相同,但是它們可能並不相等。一些面試者可能就此放棄,而另外一些還能繼續挺進,他們回答“因爲hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因爲HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事還沒有完結,面試官會繼續問:

“如果兩個鍵的hashcode相同,你如何獲取值對象?” 面試者會回答:當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然後獲取值對象。面試官提醒他如果有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷鏈表直到找到值對象。面試官會問因爲你並沒有值對象去比較,你是如何確定確定找到值對象的?除非面試者直到HashMap在鏈表中存儲的是鍵值對,否則他們不可能回答出這一題。

其中一些記得這個重要知識點的面試者會說,找到bucket位置之後,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。完美的答案!

許多情況下,面試者會在這個環節中出錯,因爲他們混淆了hashCode()和equals()方法。因爲在此之前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候纔出現。一些優秀的開發者會指出使用不可變的、聲明作final的對象,並且採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作爲鍵是非常好的選擇。

如果你認爲到這裏已經完結了,那麼聽到下面這個問題的時候,你會大吃一驚。“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?”除非你真正知道HashMap的工作原理,否則你將回答不出這道題。默認的負載因子大小爲0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因爲它調用hash方法找到新的bucket位置。

如果你能夠回答這道問題,下面的問題來了:“你瞭解重新調整HashMap大小存在什麼問題嗎?”你可能回答不上來,這時面試官會提醒你當多線程的情況下,可能產生條件競爭(race condition)。

當重新調整HashMap大小的時候,確實存在條件競爭,因爲如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因爲移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死循環了。這個時候,你可以質問面試官,爲什麼這麼奇怪,要在多線程的環境下使用HashMap呢?:)

)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章