Hashtable 和 HashMap 的區別
1. 類定義
這個從原始碼中可以直接看出來,HashMap 繼承自 AbstractMap,而 Hashtabl 繼承自 Dictionary。
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
2. 執行緒安全性
Hashtable 在很多方法定義時都會加上 synchronized
關鍵字,說明 Hashtabl 是執行緒安全的,而 HashMap 並不能保證執行緒安全。
public synchronized int size();
public synchronized boolean isEmpty();
public synchronized boolean contains(Object o);
public synchronized V get(Object key);
public synchronized V put(K key, V value);
...
3. key 和 value 是否允許 null
在 Hashtable 新增元素原始碼中,我們可以發現,如果新增元素的 value 為 null 時,會丟擲 NullPointerException。在程式內部,有這樣一行程式碼 int hash = key.hashCode
,如果新增的 key 為 null 時,此時也會丟擲空指標異常,因此,在 Hashtable 中,是不允許 key 和 value 為 null 的。
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;
}
而在 HashMap 的 put 方法中,呼叫了 putVal
方法(1.8 版本中),該方法需要有一個 int 型別的 hash
值,這個值是利用內部的 hash
方法產生的。從下面的原始碼可以看出,當 key 為 null 時,返回的 hash 值為 0,說明在 HashMap 中是允許 key=null 的情況存在的。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict){
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4. 是否提供 contains 方法
從 HashMap 的 API 可以看出,它只包含 containsKey 和 containsValue 方法。而 Hashtable 還包含了 contains 方法。
HashMap 中把 contains 方法去掉的原因主要它容易引起混淆,不如 containsKey 和 containsValue 表達的準確。
而 Hashtable 中 contains 方法也是呼叫 containsKey 方法來實現的。
public boolean contains(Object o) {
return containsKey(o);
}
5. 初始容量
Hashtable 初始容量為 11,預設的負載因子為 0,.75。HashMap 定義了兩個常量在對容器進行初始化會用到,可以看到其初始容量為 16,預設的負載因子也是為 0.75.
//---------------------Hashtable-----------------------------
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
// 這裡對桶進行初始化
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
//---------------------HashMap-----------------------------
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
6. 擴容程度
Hashtable 擴容時呼叫 rehash
方法,增加容器容量的程式碼在下面。從中可以看出,最終的容量是 newCapacity
,如果該變數在沒有大於 MAX_ARRAY_SIZE
(靜態變數,內部定義為 Integer.MAX_VALUE - 8) 之前,都是按照 oldCapacity*2 + 1 的速度增加的。
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
在 HashMap 中,擴容主要是通過 resize
方法實現的,其擴容的程式碼是這樣的 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
,可見是跟 newCap 變數有關,在正常情況下,newCapa 是按照 oldCap<<1
的速度,即每次長度變為原來的兩倍增長的。
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
7. 迭代器
本段話摘自 http://www.cnblogs.com/alexlo/archive/2013/03/14/2959233.html
HashMap 的迭代器(Iterator) 是 fail-fast 迭代器,而 Hashtable 的 enumerator 迭代器不是 fail-fast 的。所以當有其它執行緒改變了 HashMap 的結構(增加或者移除元素),將會丟擲 ConcurrentModificationException,但迭代器本身的 remove() 方法移除元素則不會丟擲 ConcurrentModificationException 異常。但這並不是一個一定發生的行為,要看 JVM 。這條同樣也是 Enumeration 和 Iterator 的區別。關於 fail-fast 機制可以檢視這篇文章。
8. hash 演算法
一下下這段分析摘自:https://www.cnblogs.com/xiaoxi/p/7233201.html
hash 演算法是將元素定位到相對應桶的位置上,在 Hashtable 中,是這樣實現 hash 演算法的。因為 Hashtable 中,其桶擴容之後長度為奇數,這種方式的雜湊取模會更加均勻(這點還是不清楚為什麼)。
int hash = key.hashCode();
// hash 不能超過 Integer.MAX_VALUE,所以要取其最小的 32 個 bit
int index = (hash & 0x7FFFFFFF) % tab.length;
在 JDK 1.8 版本中,HashMap 的 hash 方法如下。
static final int hash(Object key){
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putValu(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
...
}
- 其首先是獲取該物件的 hashCode 值,然後將 hashCode 值右移 16 位,然後將右移後的值與原來的 hashCode 做
異或
運算,返回結果。 - 在 putVal 中,通過
(n-1)&hash
獲取該物件的鍵在 hashmap 中的位置。(其中 hash 就是通過 hash 方法獲得的值),n 表示 hash 桶陣列的長度,並且該長度為 2 的 n 次方。
通常宣告 map 集合時不會指定大小,或者初始化的時候就建立一個容量很大的map 物件,所以這個通過容量大小與 key 值進行 hash 的演算法在開始的時候只會對低位進行計算,雖然容量的 2 進位制高位一開始都是 0,但是 key 的 2 進位制高位通常是有值的,因此先在 hash 方法中將 key 的 hashCode 右移 16 位在與自身異或,使得高位也可以參與hash,更大程度上減少了碰撞率。