HashMap與HashTable有什麼區別?在面試中經常會問到這樣的問題。於是,通過查閱一些資料,總結一下寫下這篇博客。
區別1:兩者誕生的時間 不同。
HashTable產生於JDK1.1,而HashMap產生於JDK1.2。從時間上來說,HashMap要比HashTable出現得晚一些。
區別2:類的繼承體系不同
可以看一下下面兩幅圖(HashMap和HashTable的類繼承圖)
從上面兩幅圖中可以看出。HashMap和HashTable都實現了Cloneable、Serializable和Map接口。但是不同之處是
HashMap繼承了抽象類AbstractMap,而HashTable繼承了抽象類Dictionary(此類已經是一個被廢棄的類)
區別3:對於Null key和 Null Value的處理不同
HashMap支持null鍵和null值的,而HashTable在遇到null鍵和null值時會拋出空指針異常。這主要是因爲代碼中對null值
的處理。可以看一下代碼:
以下是HashTable的代碼:
public synchronized V put(K key, V value) {
// 如果value爲null,拋出NullPointerException
if (value == null) {
throw new NullPointerException();
}
// 如果key爲null,在調用key.hashCode()時拋出NullPointerException
// ...
}
以下是HashMap的代碼:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 當key爲null時,調用putForNullKey特殊處理
if (key == null)
return putForNullKey(value);
// ...
}
private V putForNullKey(V value) {
// key爲null時,放到table[0]也就是第0個bucket中
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
區別4:實現原理方面的不同:主要是在數據結構和算法層面
相同點(數據結構):HashMap和HashTable都使用哈希表來存儲鍵值對,在數據結構上基本上是相同,都創建了一個繼承自
Map.Entry的私有的內部類Entry,每一個Entry對象都表示存儲在Hash表中的鍵值對。
Entry對象唯一表示一個鍵值對,有四個屬性:
-K key 鍵對象
-V value 值對象
-int hash 鍵對象的hash值
-Entryentry 指向鏈表中下一個Entry對象,可爲null,表示當前Entry對象在鏈表尾部
可以通過下圖更加清晰的看出存儲的數據結構
這樣就可以得出這樣的結論:HashMap和HashTable內部用Entry數組實現哈希表,而對於映射到同一個哈希桶
bucket(數組的同意位置)的鍵值對,使用Entry鏈表來存儲(解決hash衝突)。
算法(不同):
上一節中已經說了HashMap和HashTable的內部數據結構。HashMap和HashTable還需要有算法來將一個給定的key值計算出HashCode值,映射到確定的Hash桶(數組位置)需要有算法在hash桶的鍵值多到一定程度時,擴充hash表的大小(數組的大小)。本節主要是比較兩個類在算法層面是有什麼樣
的不同。
初始容量大小和每次擴充容量大小的不同。先看一下代碼:
以下代碼及註釋來自java.util.HashTable
// 哈希表默認初始大小爲11
public Hashtable() {
this(11, 0.75f);
}
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
// 每次擴容爲原來的2n+1
int newCapacity = (oldCapacity << 1) + 1;
// ...
}
以下代碼及註釋來自java.util.HashMap
// 哈希表默認初始大小爲2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
void addEntry(int hash, K key, V value, int bucketIndex) {
// 每次擴充爲原來的2n
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
從上面的代碼中可以看出來,對於HashTable,hash表的默認初始值是11,然後每次擴充爲原來的2n+1。
而HashMap的初始值是16,之後每次擴充爲原來的2倍。
也就是說HashTable的hash表容量儘量是素數、質數。而HashMap則總是使用2的冪作爲hash表的大小。
那它們這樣做有什麼優缺點那?
當hash表的大小爲質數時,簡單的取模哈希表的結果會更加的均勻,所以,從這一點上看,HashTable的哈希表大小的選擇會
更加的合理。但是另一方面,如果取模數是2的冪的話,我們可以使用另外一種方法會更加的方便:位運算,效率會大大高於
除法運算得到的結果。所以,從Hash計算的效率上,又是HashMap更勝一籌。
所以,hashmap爲了解決取模不均勻的問題,又對hash算法做了一些改動。具體我們看看,在獲取了key對象的hashCode之後,
HashTable和HashMap分別是怎樣將他們哈希到確定的hash桶(Entry數組位置)中的。
以下代碼及註釋來自java.util.HashTable
// hash 不能超過Integer.MAX_VALUE 所以要取其最小的31個bit
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
// 直接計算key.hashCode()
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}
以下代碼及註釋來自java.util.HashMap
int hash = hash(key);
int i = indexFor(hash, table.length);
// 在計算了key.hashCode()之後,做了一些位運算來減少哈希衝突
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by【再哈希算法】
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 取模不再需要做除法
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
我們可以通過上面的代碼可以看出,HashMap由於使用了2的冪次方,所以在位運算取模時,引入了hash衝突加劇的問題。爲了解決這個問題,HashMap調用了對象的hashCode()方法之後,又重新做了一些位運算再次打散數據(再hash)。
區別5:線程安全
HashTable是線程同步的,而HashMap不是的。可以看一下代碼:
以下代碼及註釋來自java.util.HashTable
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
從代碼中我們可以看出來:在公開的方法中,HashTable中的方法都是用了synchronized描述符。