從源碼的角度分析Hashtable和HashMap的區別

引言

面試中我們經常被問到這樣的問題:”請說說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。

總結

  1. 創建對象時,Hashtable初始化了加載因子(0.75f)、數組長度(11)、閥值、;而HashMap只初始化了加載因子(0.75f),它在第一次put時初始化數組長度(16)
  2. HashMap內部的存儲結構是Node,而Hashtable內部的存儲結構Entry。雖然名稱不同,但它們有相同的數據結構。並且它們都實現了Map.Entry接口;
  3. Hashtable的put方法是線程安全的,而HashMap的put方法不是;
  4. 擴容時Hashtable長度變爲原來2倍+1;而HashMap長度爲原來2倍;
  5. 使用put方法時table的value值不可以爲null,Map可以。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章