Hashtable和HashMap引發的血案 – JAVA編程語言程序開發技術文章

FROM:王胖子的黑板報


人物:


王小胖:性別:男。程序員,工作經驗1 year。愛好:吃肉、電玩、馬小花。特技:吃肉不用考慮胃的容量。


馬小花:性別:女。學生,工作經驗0 year。愛好:蛋糕、臭美、王小胖。特技:能夠降服王小胖……


 


/**2011年2月,電影《將愛情進行到底》火得不得瞭。周末,小胖也陪著小花去看這部電影。放映中,小花被影片中的靖哥哥和杜拉拉感動的一沓糊塗,而小胖則心裡暗自後悔沒有買一袋大爆米花來打發這無聊的時間。影片結束,小花已經是鼻涕一把淚一把,小胖也隻有裝模作樣地抽動瞭幾下鼻子,一心隻想著一會兒是吃麥當勞還是必勝客。*/


回到傢中,小胖和小花各自玩著電腦。


小花:胖子,你知道Hashtable和HashMap的區別嗎?


小胖:略知。


小花:……裝什麼!!給我講講!!!


小胖:好的……


第一個區別就先來說說繼承關系吧。


 如果你在baidu裡google一下(技術類文章的搜索還是推薦google),會發現網上的大致說法與“由於Java發展的歷史原因。Hashtable是基於陳舊的Dictionary類的,HashMap是Java 1.2引進的Map接口的一個實現。”相同。這種說法沒有錯,但是胖子覺得不夠準確,特別是對於我們這種大眾菜鳥來說,如果不去深究的話,可能就會造成一些理解上的差異。簡單的認為Hashtable沒有繼承Map接口。胖子之前就犯過這樣的錯誤(胖子承認自己笨,是真笨……) 。


       小花:那你怎麼知道它們兩個各自的繼承關系呢?胖子。


我們可以參考一下最新的JDK1.6的源碼,看看這兩個類的定義:


 



public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {…}
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {…}


 


 



可以看到hashtable也是繼承瞭Map接口。它們的不同是Hashtable(since JDK1.0)就繼承瞭Dictionary這個抽象類,而HashMap(since JDK1.2)繼承的則是AbstractMap這個抽象類。因為在Hashtable中看到繼承Map後所實現的方法是JDK1.2版本時加上去的,所以胖子猜想可能是在JDK 1.2開發時Sun工程師出於統一的考慮使得Hashtable也繼承瞭Map接口。


       小花:哦,原來JDK源碼還能看出來這個。


       小胖:……後面還能看出更多東西的。


       小花:好期待啊。


 



第二個區別我們從同步和並發性上來說說它們兩個的不同。


 



可以通過這兩個類得源碼來分析,Hashtable中的主要方法都做瞭同步處理,而HashMap則沒有。可以說Hashtable在默認情況支持同步,而HashMap在默認情況下是不支持的。我們在多線程並發的環境下,可以直接使用Hashtable,但是要使用HashMap的話就要自己增加同步處理瞭。對HashMap的同步處理可以使用Collections類提供的synchronizedMap靜態方法;或者直接使用JDK5.0之後提供的java.util.concurrent包裡的ConcurrentHashMap類。


小胖:synchronizedMap靜態方法和ConcurrentHashMap類我會以後再給你詳細講一下的。肥婆。


小花:你保證啊。鑰匙忘瞭你知道後果的。


小胖:好的……


 



第三個區別就是它們對於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;
//…
}
  
在我們使用上面的方法時,如參數value為null,可以從代碼中直接看出程序會拋出NullPointerException;而在key為null時,則會在“int hash = key.hashCode();“這段計算Hash值的過程中拋出NullPointerException。而在在HashMap中,允許null作為key存在,並且和其他key的特性一樣,這樣的null值key隻能有一個;另外HashMap允許多個value為null。這樣大傢就要註意瞭, HashMap中就不能用get(key)方法來判斷HashMap中是否存在某個key,因為value為null和不存在該key的Entry都會返回null值,而應該用containsKey()方法來判斷瞭。


 



import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class TestCase {
public static void main(String[] args) {
Map<Integer,String> hashMap = new HashMap<Integer,String>();
hashMap.put(0, null);
hashMap.put(1, “one”);
hashMap.put(2, “two”);
hashMap.put(null, “null”);
for(Entry<Integer, String> e : hashMap.entrySet()) {
System.out.println(“Key: ” + e.getKey() + ” — Value: ” + e.getValue());
}
System.out.println(hashMap.get(0));
System.out.println(hashMap.get(4));
System.out.println(“Contains key 0 ? :” + hashMap.containsKey(0));
System.out.println(“Contains key 4 ? :” + hashMap.containsKey(4));
System.out.println(“Contains value null ? :” + hashMap.containsValue(null));
}
}
 
結果:


 



Key: null — Value: null
Key: 0 — Value: null
Key: 1 — Value: one
Key: 2 — Value: two
null
null
Contains key 0 ? :true
Contains key 4 ? :false
Contains value null ? :true


 


HashMap對於null值key的處理網上有說“null 用new Object()來代替,其Entry.hashCode=0,而且在取出的時候還會還回null的。”胖子我在讀取源碼的過程中看到瞭null值的hash值確實是0 (內部實現的數組中的index也是),但是能力有限沒有看到轉為new Object()的過程。


小花: 原來hashMap的containsKey還有這麼個陷阱,以後肥婆要小心瞭。


第四個不同就是它們兩個Hash值的獲取方式瞭。


 



還是通過源代碼源代碼,Hashtable是直接使用key對象的hash值。


 


 


 


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();//hashcode
int index = (hash & 0x7FFFFFFF) % tab.length;
//…
}
 


 


而HashMap則是利用key對象的hash值重新計算一個新的hash值。


 


 


 


public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());//hashcode
int i = indexFor(hash, table.length);
//…
}
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}


 


小花:胖子,都用瞭hash算法,你給我講講Hash算法吧。


小胖:嗯……以後的,今天我比較忙(其實是不會)。


小花:你是不是不會啊?嘿嘿(壞笑)。


小胖:什麼不會……談下一話題……


 


第五個不同就是Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。


 



HashMap中內部數組的初始容量是16, 加載因子為0.75,而且數組容量增容後也要是2指數次冪:


 


 


 


/**
* The default initial capacity – MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 


 


HashTable中的內部數組的初始容量是11,加載因子也是0.75數組的增容方式為(oldCapacity * 2 + 1):


 


 


 


public Hashtable() {
this(11, 0.75f);
}
protected void rehash() {
int oldCapacity = table.length;
Entry[] oldMap = table;
int newCapacity = oldCapacity * 2 + 1;
//…
}
 


 


第六個不同我們從它們兩個遍歷方式的內部實現上來說。


Hashtable HashMap都使用瞭 Iterator。而由於歷史原因,Hashtable還使用瞭Enumeration的方式 。


小花:Iterator和Enumeration的區別是什麼啊?給我講講。


小胖:我不是說我沒有時間嘛,下回的。


小花:我都記下來,省得你給我混過去。(拿起筆開始記賬中)


小胖:……(緊張)


第七個不同時它們的拷貝構造函數的不同。


 



依然是通過查看源碼,可以發現它們兩個對於拷貝函數初始容量的不同值。


HashMap的實現是:

發佈留言