【java】 HashMap的工作原理+HashMap和Hashtable的區別+HashMap和HashSet的區別
本文由 ImportNew - 唐小娟 翻譯自 Javarevisited。
HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程式設計師都知道HashMap,都知道哪裡要用HashMap,知道Hashtable和HashMap之間的區別,那麼為何這道面試題如此特殊呢?是因為這道題考察的深度很深。這題經常出現在高階或中高階面試中。投資銀行更喜歡問這個問題,甚至會要求你實現HashMap來考察你的程式設計能力。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加複雜。讓我們開始探索的旅程吧!
先來些簡單的問題
“你用過HashMap嗎?” “什麼是HashMap?你為什麼用到它?”
幾乎每個人都會回答“是的”,然後回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Hashtable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經用過HashMap,而且對它相當的熟悉。但是面試官來個急轉直下,從此刻開始問出一些刁鑽的問題,關於HashMap的更多基礎的細節。面試官可能會問出下面的問題:
“你知道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呢?:)
熱心的讀者貢獻了更多的關於HashMap的問題:
- 為什麼String, Interger這樣的wrapper類適合作為鍵? String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那麼就不能從HashMap中找到你想要的物件。不可變性還有其他的優點如執行緒安全。如果你可以僅僅通過將某個field宣告成final就能保證hashCode是不變的,那麼請這麼做吧。因為獲取物件的時候要用到equals()和hashCode()方法,那麼鍵物件正確的重寫這兩個方法是非常重要的。如果兩個不相等的物件返回不同的hashcode的話,那麼碰撞的機率就會小些,這樣就能提高HashMap的效能。
- 我們可以使用自定義的物件作為鍵嗎? 這是前一個問題的延伸。當然你可能使用任何物件作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當物件插入到Map中之後將不會再改變了。如果這個自定義物件時不可變的,那麼它已經滿足了作為鍵的條件,因為當它建立之後就已經不能改變了。
- 我們可以使用CocurrentHashMap來代替Hashtable嗎?這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步效能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的執行緒安全性。看看這篇部落格檢視Hashtable和ConcurrentHashMap的區別。
我個人很喜歡這個問題,因為這個問題的深度和廣度,也不直接的涉及到不同的概念。讓我們再來看看這些問題設計哪些知識點:
- hashing的概念
- HashMap中解決碰撞的方法
- equals()和hashCode()的應用,以及它們在HashMap中的重要性
- 不可變物件的好處
- HashMap多執行緒的條件競爭
- 重新調整HashMap的大小
總結
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取物件。當我們將鍵值對傳遞給put()方法時,它呼叫鍵物件的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值物件。當獲取物件時,通過鍵物件的equals()方法找到正確的鍵值對,然後返回值物件。HashMap使用連結串列來解決碰撞問題,當發生碰撞了,物件將會儲存在連結串列的下一個節點中。 HashMap在每個連結串列節點中儲存鍵值對物件。
當兩個不同的鍵物件的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的連結串列中。鍵物件的equals()方法用來找到鍵值對。
因為HashMap的好處非常多,我曾經在電子商務的應用中使用HashMap作為快取。因為金融領域非常多的運用Java,也出於效能的考慮,我們會經常用到HashMap和ConcurrentHashMap。你可以檢視更多的關於HashMap的文章:
HashMap和Hashtable的區別
HashMap和Hashtable的比較是Java面試中的常見問題,用來考驗程式設計師是否能夠正確使用集合類以及是否可以隨機應變使用多種思路解決問題。HashMap的工作原理、ArrayList與Vector的比較以及這個問題是有關Java 集合框架的最經典的問題。Hashtable是個過時的集合類,存在於Java API中很久了。在Java 4中被重寫了,實現了Map介面,所以自此以後也成了Java集合框架中的一部分。Hashtable和HashMap在Java面試中相當容易被問到,甚至成為了集合框架面試題中最常被考的問題,所以在參加任何Java面試之前,都不要忘了準備這一題。
這篇文章中,我們不僅將會看到HashMap和Hashtable的區別,還將看到它們之間的相似之處。
HashMap和Hashtable的區別
HashMap和Hashtable都實現了Map介面,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:執行緒安全性,同步(synchronization),以及速度。
- HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。
- HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是執行緒安全的,多個執行緒可以共享一個Hashtable;而如果沒有正確的同步的話,多個執行緒是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴充套件性更好。
- 另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。
- 由於Hashtable是執行緒安全的也是synchronized,所以在單執行緒環境下它比HashMap要慢。如果你不需要同步,只需要單一執行緒,那麼使用HashMap效能要好過Hashtable。
- HashMap不能保證隨著時間的推移Map中的元素次序是不變的。
要注意的一些重要術語:
1) sychronized意味著在一次僅有一個執行緒能夠更改Hashtable。就是說任何執行緒要更新Hashtable時要首先獲得同步鎖,其它執行緒要等到同步鎖被釋放之後才能再次獲得同步鎖更新Hashtable。
2) Fail-safe和iterator迭代器相關。如果某個集合物件建立了Iterator或者ListIterator,然後其它的執行緒試圖“結構上”更改集合物件,將會丟擲ConcurrentModificationException異常。但其它執行緒可以通過set()方法更改集合物件是允許的,因為這並沒有從“結構上”更改集合。但是假如已經從結構上進行了更改,再呼叫set()方法,將會丟擲IllegalArgumentException異常。
3) 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。
我們能否讓HashMap同步?
HashMap可以通過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);
結論
Hashtable和HashMap有幾個主要的不同:執行緒安全以及速度。僅在你需要完全的執行緒安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。
HashMap和HashSet的區別
HashMap和HashSet的區別是Java面試中最常被問到的問題。如果沒有涉及到Collection框架以及多執行緒的面試,可以說是不完整。而Collection框架的問題不涉及到HashSet和HashMap,也可以說是不完整。HashMap和HashSet都是collection框架的一部分,它們讓我們能夠使用物件的集合。collection框架有自己的介面和實現,主要分為Set介面,List介面和Queue介面。它們有各自的特點,Set的集合裡不允許物件有重複的值,List允許有重複,它對集合中的物件進行索引,Queue的工作原理是FCFS演算法(First Come, First Serve)。
首先讓我們來看看什麼是HashMap和HashSet,然後再來比較它們之間的分別。
什麼是HashSet
HashSet實現了Set介面,它不允許集合中有重複的值,當我們提到HashSet時,第一件事情就是在將物件儲存在HashSet之前,要先確保物件重寫equals()和hashCode()方法,這樣才能比較物件的值是否相等,以確保set中沒有儲存相等的物件。如果我們沒有重寫這兩個方法,將會使用這個方法的預設實現。
public boolean add(Object o)方法用來在Set中新增元素,當元素值重複時則會立即返回false,如果成功新增的話會返回true。
什麼是HashMap
HashMap實現了Map介面,Map介面對鍵值對進行對映。Map中不允許重複的鍵。Map介面有兩個基本的實現,HashMap和TreeMap。TreeMap儲存了物件的排列次序,而HashMap則不能。HashMap允許鍵和值為null。HashMap是非synchronized的,但collection框架提供方法能保證HashMap synchronized,這樣多個執行緒同時訪問HashMap時,能保證只有一個執行緒更改Map。
public Object put(Object Key,Object value)方法用來將元素新增到map中。
你可以閱讀這篇文章看看HashMap的工作原理,以及這篇文章看看HashMap和HashTable的區別。
HashSet和HashMap的區別
*HashMap* | *HashSet* |
HashMap實現了Map介面 | HashSet實現了Set介面 |
HashMap儲存鍵值對 | HashSet僅僅儲存物件 |
使用put()方法將元素放入map中 | 使用add()方法將元素放入set中 |
HashMap中使用鍵物件來計算hashcode值 | HashSet使用成員物件來計算hashcode值,對於兩個物件來說hashcode可能相同,所以equals()方法用來判斷物件的相等性,如果兩個物件不同的話,那麼返回false |
HashMap比較快,因為是使用唯一的鍵來獲取物件 | HashSet較HashMap來說比較慢 |
原文連結: Javarevisited 翻譯: ImportNew.com - 唐小娟
譯文連結: http://www.importnew.com/7099.html