引言
俗話說:程序 = 數據結構 + 算法,搞定數據結構就相當于搞定了一半。數據結構是以某種形式將數據組織在一起的集合,它不僅存儲數據,還支持訪問和處理數據的操作。Java 提供了幾個能有效地組織和操作數據的數據結構,這些數據結構通常稱為 Java 集合框架。在平常的學習開發中,靈活熟練地使用這些集合框架,可以很明顯地提高我們的開發效率,當然僅僅會用還是不夠的,理解其中的設計思想與原理才能更好地提高我們的開發水平。



在 Java 2 之前,Java 是沒有完整的集合框架的。它只有一些簡單的可以自擴展的容器類,比如 Vector
,Stack
, Hashtable
等。這些容器類在使用的過程中由于效率問題飽受詬病,因此在Java 2中,Java設計者們進行了大刀闊斧的整改,重新設計,于是就有了現在的集合框架。需要注意的是,之前的那些容器類庫并沒有被棄用而是進行了保留,主要是為了向下兼容的目的,但我們在平時使用中還是應該盡量少用。
Java 集合框架經典問題的第一篇,就來講講 HashMap
和 Hashtable
的異同之處
HashMap & Hashtable
- 定義
- 原理
- 相同點
- 區別
- 線程安全
- 存儲形式
- 迭代器
- 使用性能
- 面試題
- 總結
定義
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
Hashtable
繼承于陳舊的 Dictionary
類,HashMap繼承于 AbstractMap
原理


HashMap基于hashing原理,我們通過 put
() 和 get
() 方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的 hashCode
() 方法來計算 hashcode
,讓后找到 bucket
位置來儲存值對象。當獲取對象時,通過鍵對象的 equals
() 方法找到正確的鍵值對,然后返回值對象。 HashMap
使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap
在每個鏈表節點中儲存鍵值對對象。
當兩個不同的鍵對象的 hashcode
相同時會發生什么? 它們會儲存在同一個 bucket
位置的鏈表中。key 對象的 equals
() 方法用來找到鍵值對,key的存儲是唯一的,當 put()
方法執行時 key 的 equals()
返回true,則不再存儲這個 (key, value)。
關鍵字:hashcode、equals
相同點
HashMap
和 Hashtable
采用相同的存儲機制,二者實現基本一致。
區別
1.線程安全
//Hashtable
public synchronized V put(K key, V value)
//HashMap
public V put(K key, V value)
Hashtable
的方法基本都有 synchronized
關鍵字修飾,是線程安全的,HashMap
是不是線程安全的,但可以通過以下這種方式來同步
Map m = Collections.synchronizeMap(hashMap)
如果你不需要同步,則使用HashMap
即可
2.存儲形式
//Hashtable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
Hashtable
不允許存儲 null
的 key 和 value, HashMap
則可以。
3.迭代器
HashMap
的迭代器Iterator
是 fail-fast
迭代器,而Hashtable
的enumerator
迭代器不是fail-fast
的,所以當有其它線程改變了Hash
Map的結構(增加或者移除元素),將會拋出ConcurrentModificationException
,但迭代器本身的remove()
方法移除元素則不會拋出ConcurrentModificationException
異常。
4.使用性能
HashMap
的性能無疑要好于 Hashtable
,因為 Hashtable
每次使用都要同步,這會損失一定的速度,單線程運行時使用 HashMap
。在 Java 5 之后, ConcurrentHashMap
成了 Hashtable
的替代品,所以需要同步時,請使用 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呢?)
總結
- 注意理解存儲過程中
hashcode()
和equals()
方法的作用 - 了解常見的哈希函數構造方法和解決沖突方法,如
HashMap
用的就是拉鏈法來解決沖突 -
HashMap
除了線程不安全外,其他基本都優于Hashtable
-
Hashtable
基本已經被棄用,在 Java 5 之后用ConcurrentHashMap
來替代