HashMap 與HashTable的區別

HashMap與Hashtable的區別是面試中經常遇到的一個問題。這個問題看似簡單,但如果深究進去,也能瞭解到不少知識。本文對兩者從來源,特性,算法等多個方面進行對比總結。力爭多角度,全方位的展示二者的不同,做到此問題的終結版。

1 作者 
Hashtable的作者: 
這裏寫圖片描述 
HashMap的作者: 
這裏寫圖片描述

Hash Map的作者比Hashtable的作者多了著名頂頂的併發大神Doug Lea。他寫了util.concurrent包。著有併發編程聖經Concurrent Programming in Java: Design Principles and Patterns 一書。他的個人主頁: http://g.oswego.edu/

Josh Bloch 爲領導了衆多Java平臺特性的設計和實現,其中包括Java Collection框架、java.math包以及assert機制。著有 Effective Java 一書。

Arthur van Hoff最早任職於硅谷的Sun Microsystems公司,從事Java程序語言的早期開發工作。設計並實現了JDK 1.0的許多方面,包括Java編譯器、Java調試器、許多標準Java類以及HotJava瀏覽器。隨後創立了多家成功的企業,其中包括Marimba(1999年IPO)、Strangeberry(後被TiVo收購)、ZING(後被Dell收購)和Ellerdale(後被Flipboard收購)。Java命名來源有這麼一種說法,來源於開發人員名字的組合:James Gosling、Arthur Van Hoff和Andy Bechtolsheim首字母的縮寫。

Neal Gafter是Java SE 4和5語言增強的主要設計者和實現者,他的Java閉包實現贏得了OpenJDK創新者挑戰賽的大獎。他也在繼續參與SE 7和8的語言發展。之前Neal在爲Google的在線日曆工作,也曾經是C++標準委員會的一員,並曾在Sun微系統公司,MicroTec研究院和德州儀器領導開發C和C++編譯器。如今Neal在微軟開發.NET平臺編程語言。Neal是《Java Puzzlers:Traps, Pitfalls and Corner Cases》(Addison Wesley,2005)一書的合作者。他擁有羅徹斯特大學計算機科學的博士學位。

可見這些作者都是java乃至整個it領域大名鼎鼎的人物。也只有這些大師級人物才能寫出HashMap這麼大道至簡的數據類型了。

2 產生時間 
Hashtable是java一開始發佈時就提供的鍵值映射的數據結構,而HashMap產生於JDK1.2。雖然Hashtable比HashMap出現的早一些,但是現在Hashtable基本上已經被棄用了。而HashMap已經成爲應用最爲廣泛的一種數據類型了。造成這樣的原因一方面是因爲Hashtable是線程安全的,效率比較低。另一方面可能是因爲Hashtable沒有遵循駝峯命名法吧。。。

3 繼承的父類不同 
HashMap和Hashtable不僅作者不同,而且連父類也是不一樣的。HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個接口

這裏寫圖片描述

這裏寫圖片描述

Dictionary類是一個已經被廢棄的類(見其源碼中的註釋)。父類都被廢棄,自然而然也沒人用它的子類Hashtable了。 
NOTE: This class is obsolete. New implementations should 
* implement the Map interface, rather than extending this class.

4 對外提供的接口不同 
Hashtable比HashMap多提供了elments() 和contains() 兩個方法。

elments() 方法繼承自Hashtable的父類Dictionnary。elements() 方法用於返回此Hashtable中的value的枚舉。

contains()方法判斷該Hashtable是否包含傳入的value。它的作用與containsValue()一致。事實上,contansValue() 就只是調用了一下contains() 方法。

這裏寫圖片描述

5 對Null key 和Null value的支持不同 
Hashtable既不支持Null key也不支持Null value。Hashtable的put()方法的註釋中有說明。 
這裏寫圖片描述

當key爲Null時,調用put() 方法,運行到下面這一步就會拋出空指針異常。因爲拿一個Null值去調用方法了。 
這裏寫圖片描述

當value爲null值時,Hashtable對其做了限制,運行到下面這步也會拋出空指針異常。 
這裏寫圖片描述

HashMap中,null可以作爲鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值爲null。當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值爲null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

6 線程安全性不同 
Hashtable是線程安全的,它的每個方法中都加入了Synchronize方法。在多線程併發的環境下,可以直接使用Hashtable,不需要自己爲它的方法實現同步

HashMap不是線程安全的,在多線程併發的環境下,可能會產生死鎖等問題。具體的原因在下一篇文章中會詳細進行分析。使用HashMap時就必須要自己增加同步處理,

雖然HashMap不是線程安全的,但是它的效率會比Hashtable要好很多。這樣設計是合理的。在我們的日常使用當中,大部分時間是單線程操作的。HashMap把這部分操作解放出來了。當需要多線程操作的時候可以使用線程安全的ConcurrentHashMap。ConcurrentHashMap雖然也是線程安全的,但是它的效率比Hashtable要高好多倍。因爲ConcurrentHashMap使用了分段鎖,並不對整個數據進行鎖定。

7 遍歷方式的內部實現上不同 
Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。

HashMap的Iterator是fail-fast迭代器。當有其它線程改變了HashMap的結構(增加,刪除,修改元素),將會拋出ConcurrentModificationException。不過,通過Iterator的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行爲,要看JVM。

JDK8之前的版本中,Hashtable是沒有fast-fail機制的。在JDK8及以後的版本中 ,HashTable也是使用fast-fail的, 源碼如下: 
這裏寫圖片描述

modCount的使用類似於併發編程中的CAS(Compare and Swap)技術。我們可以看到這個方法中,每次在發生增刪改的時候都會出現modCount++的動作。而modcount可以理解爲是當前hashtable的狀態。每發生一次操作,狀態就向前走一步。設置這個狀態,主要是由於hashtable等容器類在迭代時,判斷數據是否過時時使用的。儘管hashtable採用了原生的同步鎖來保護數據安全。但是在出現迭代數據的時候,則無法保證邊迭代,邊正確操作。於是使用這個值來標記狀態。一旦在迭代的過程中狀態發生了改變,則會快速拋出一個異常,終止迭代行爲。

8 初始容量大小和每次擴充容量大小的不同 
Hashtable默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1。HashMap默認的初始化大小爲16。之後每次擴充,容量變爲原來的2倍。

創建時,如果給定了容量初始值,那麼Hashtable會直接使用你給定的大小,而HashMap會將其擴充爲2的冪次方大小。也就是說Hashtable會盡量使用素數、奇數。而HashMap則總是使用2的冪作爲哈希表的大小。

之所以會有這樣的不同,是因爲Hashtable和HashMap設計時的側重點不同。Hashtable的側重點是哈希的結果更加均勻,使得哈希衝突減少。當哈希表的大小爲素數時,簡單的取模哈希的結果會更加均勻。而HashMap則更加關注hash的計算效率問題。在取模計算時,如果模數是2的冪,那麼我們可以直接使用位運算來得到結果,效率要大大高於做除法。HashMap爲了加快hash的速度,將哈希表的大小固定爲了2的冪。當然這引入了哈希分佈不均勻的問題,所以HashMap爲解決這問題,又對hash算法做了一些改動。這從而導致了Hashtable和HashMap的計算hash值的方法不同 
9 計算hash值的方法不同 
爲了得到元素的位置,首先需要根據元素的 KEY計算出一個hash值,然後再用這個hash值來計算得到最終的位置。

Hashtable直接使用對象的hashCode。hashCode是JDK根據對象的地址或者字符串或者數字算出來的int類型的數值。然後再使用除留餘數發來獲得最終的位置。 
這裏寫圖片描述

Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。 
HashMap爲了提高計算效率,將哈希表的大小固定爲了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。位運算比除法的效率要高很多。

HashMap的效率雖然提高了,但是hash衝突卻也增加了。因爲它得出的hash值的低位相同的概率比較高,而計算位運算

爲了解決這個問題,HashMap重新根據hashcode計算hash值後,又對hash值做了一些運算來打散數據。使得取得的位置更加分散,從而減少了hash衝突。當然了,爲了高效,HashMap只做了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提升給抵消掉。

這裏寫圖片描述

附上關於這個問題的說明: 
Joshua Bloch: The downside of using a power-of-two is that the resulting hash table is very sensitive to the quality of the hash function (hashCode). It is imperative that any change in the input must affect the low order bits of the hash value. (Ideally, it should affect all bits of the hash value with equal likelihood.) Because we have no assurance that this is true, we put in a secondary (or “defensive”) hash function when we switched to the power-of-two hash table. This hash function is applied to the results of hashCode before masking off the low order bits. Its job is to scatter the information over all the bits, and in particular, into the low order bits. Of course it has to run very fast, or you lose the benefit of switching to the power-of-two-sized table. The original secondary hash function in 1.4 turned out to be insufficient. We knew that this was a theoretical possibility, but we thought that it didn’t affect any practical data sets. We were wrong. The replacement secondary hash function (which I developed with the aid of a computer) has strong statistical properties that pretty much guarantee good bucket distribution.

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