引言
面試中我們經常被問到這樣的問題:”請說說Hashtable和HashMap的區別?”。
通過搜索引擎,我們能輕易找到 許多答案。這些答案詳細比較了兩者的不同。但是往往停留在”知其然“的階段,只是用文字列出了兩者的不同。因此,等過一些日子我們再來回顧這個問題 時,似乎一切又歸零了(至少對於記憶不好的我來說是這樣的)。今天,我打算從源碼的角度來分析分析它們的區別,做到不僅”知其然“,更能”知其所以然“。 有興趣的話,不妨隨我一道,來看看Hashtable和HashMap的世界是什麼樣的。
注意:以下源碼來自Oracle JDK1.8。如果您在Android SdK中查看源碼發現與文中所列源碼不一致,請不要驚慌。因爲android SDK中JDK源碼採用Apache的開源項目Harmony。
數據結構
爲了便於比較,我們將兩者放在一起:首先,我們各實例化一個Hashtable和HashMap:
HashMap hashMap = new HashMap();
Hashtable hashtable = new Hashtable();
接着,進入它們的構造方法,讓我們來看看,裏面都有什麼:
//hashMap構造函數
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//DEFAULT_LOAD_FACTOR的定義
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hashmap中table申明
transient Node<K,V>[] table;
//hashtable構造函數
public Hashtable() {
this(11, 0.75f);
}
//進入this方法
public Hashtable(int initialCapacity, float loadFactor) {
//省略異常判斷代碼...
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
這裏有幾個變量解釋下: loadFactor:加載因子。兩種數據結構中都有這個變量,默認都爲0.75f;
threshold:閥值。當Hashtable擴容時,先判斷當前長度是否超過這個threshold值,來決定是否擴容,由此可見,創建一個Hashtable對象時,初始化了loadFactor,table長度11,閥值爲11*0.75 = 8(取整)。而HashMap擴容時是否也有閥值呢?答案是肯定的,只不過它的初始化並不在這裏,我們下面會介紹到;
當然你也可以在創建對象的時候指定它們的加載因子和閥值,如;
Hashtable table = new Hashtable(20,0.8f);
HashMap map = new HashMap(20,0.8f);
table:HashMap和Hashtable中都有一個數組table,HashMap中table類型爲Node泛型。Hashtable中table類型爲Entry泛型。雖然它們名稱不同,但都有相同的數據結構,並且都實現了Map.Entry接口,它們內部都有四個屬性,分別是,hash,key,value,next:
//HashMap-Node屬性
final int hash; //用於判斷檢驗的key是否相同
final K key; //存入的key
V value; //存入的value
Node<K,V> next; //指針
由此可見,Node爲HashMap中最基本數據結構,Entry爲Hashtable中最基本數據結構。我們調用put方法時添加的鍵值對都是存在table數組中的。
put方法
我們再來看看它們在put方法上的區別:
HashMap的put方法
首先來分析HashMap的put方法
//HashMap put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
接着進入putVal方法,代碼如下:
//HashMap
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//...
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
從第5行代碼中可知,putVal方法先判斷table是否爲null或者長度是否爲0,當我們使用構造函數創建HashMap對象時,table並沒有初始化,所以table爲空,條件成立(也就是第一次調用put方法)進入resize()方法:
//HashMap resize()方法
//resize方法的描述:Initializes or doubles table size.
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
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);
}
//...
threshold = newThr;
//.fangfa..
return newTab;
}
從描述中,我們得到信息,resize方法承擔了兩個功能: 1. 初始化
2. 2倍擴容
現在我們就來仔細看看這個實現過程:
- 當第一次調用put方法時,table爲null,相應的oldtab爲null。根據第5行代碼得到oldCap=0;隨即,代碼進入第20行,對newCap和newThr進行初始化:DEFAULT_INITIAL_CAPACITY=16;閥值newThr爲16*0.75=12
- 如果table不爲空,則進入第9行代碼執行,先判斷table數組長度是否達到了上限值,如果達到了,則將原table返回,也就是此次put的數據並不會添加到集合中去。如果符合擴容條件則執行:newCap = oldCap << 1進行擴容,也就是擴容爲原來的兩倍。相應的閥值也擴爲原來兩倍:newThr = oldThr << 1
Hashtable的put方法:
public synchronized V put(K key, V value) {
// table的value值不可以爲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的put方法是由synchronized修飾的,也就是它是線程安全的。這恐怕是它與HashMap的put方法最大的區別了。
在for循環中根據hash值查找key在原來集合中是否已經存在,如果存在,則替換value值。如果不存在則使用addEntry()方法將新的鍵值對加入。
在插入新值前,先做長度校驗,判斷長度是否溢出(大於閥值)。如果溢出,則進行擴容,擴容機制:int newCapacity = (oldCapacity << 1) + 1。
總結
- 創建對象時,Hashtable初始化了加載因子(0.75f)、數組長度(11)、閥值、;而HashMap只初始化了加載因子(0.75f),它在第一次put時初始化數組長度(16)
- HashMap內部的存儲結構是Node,而Hashtable內部的存儲結構Entry。雖然名稱不同,但它們有相同的數據結構。並且它們都實現了Map.Entry接口;
- Hashtable的put方法是線程安全的,而HashMap的put方法不是;
- 擴容時Hashtable長度變爲原來2倍+1;而HashMap長度爲原來2倍;
- 使用put方法時table的value值不可以爲null,Map可以。