Java 集合 - HashTable 解析

一、前言


Hashtable 與 HashMap 都是 Map 族中較爲常用的實現,也都是 Java Collection Framework 的重要成員,它們的本質都是 鏈表數組。下面我們來學習一下 HashTable。

 

二、HashTable 解析


2.1 定義

Hashtable 和 HashMap 既是 Java Collection Framework 的重要成員,也是 Map 族(如下圖所示)的核心成員,二者的底層實現都是一個鏈表數組,具有尋址容易、插入和刪除也容易的特性。事實上 HashMap 幾乎可以等價於 Hashtable,除了 HashMap 是非線程安全的並且可以接受 null 鍵和 null 值。關於 HashMap 可以看我之前的 Java 集合 - HashMap 解析

 Hashtable 實現了 Map 接口,並繼承 Dictionary 抽象類 (已過時,新的實現應該實現 Map 接口而不是擴展此類),其在 JDK 中的定義爲:

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

    private transient Entry[] table;   // 由Entry對象組成的鏈表數組

    private transient int count;   // Hashtable中Entry對象的個數

    private int threshold;   // Hashtable進行擴容的閾值

    private float loadFactor;   // 在其容量自動增加之前可以達到多滿的一種尺度,默認爲0.75

    private transient int modCount = 0;   // 記錄Hashtable生命週期中結構性修改的次數
...
}

與 HashMap 類似,Hashtable 也包括五個成員變量,分別是 table 數組、Hashtable 中 Entry 個數 count、Hashtable 的閾值 threshold、Hashtable 的負載因子 loadFactor 和 Hashtable 結構性修改次數 modCount。下面分別給出這五個成員的具體內涵:

  • Entry 數組 table: 一個由 Entry 對象組成的鏈表數組,table 數組的每一個數組成員就是一個鏈表;
  • Entry 個數 count: Hashtable 中 Entry 對象的個數;
  • 閾值 threshold: Hashtable 進行擴容的閾值;
  • 負載因子 loadFactor: 在其容量自動增加之前可以達到多滿的一種尺度,默認爲0.75;
  • 結構性修改次數 modCount: 記錄 Hashtable 生命週期中結構性修改的次數,便於快速失敗(所謂快速失敗是指其在併發環境中進行迭代操作時,若其他線程對其進行了結構性的修改,這時迭代器能夠立馬感知到並且立即拋出 ConcurrentModificationException 異常,而不是等到迭代完成之後才告訴你(你已經出錯了));

2.2 構造函數

Hashtable 一共提供了四個構造函數,其中默認無參的構造函數和參數爲Map的構造函數爲 Java Collection Framework 規範的推薦實現,其餘兩個構造函數則是Hashtable專門提供的。

1、Hashtable(int initialCapacity, float loadFactor)

  該構造函數意在構造一個指定初始容量和指定負載因子的空 Hashtable,其源碼如下:

    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;  // 初始容量完全由用戶隨意指定,不必是2的n次冪(不同於HashMap)
        this.loadFactor = loadFactor;  
        table = new Entry[initialCapacity];   // 創建指定大小爲initialCapacity的鏈表數組
        threshold = (int) (initialCapacity * loadFactor);   // HashTable的擴容閾值
    }

2、Hashtable()

  該構造函數意在構造一個具有默認初始容量(11)和默認負載因子(0.75f)的空 Hashtable,是 Java Collection Framework 規範推薦提供的,其源碼如下:

    public Hashtable() {
        this(11, 0.75f);   // 默認容量是11,不同於HashMap的默認初始容量16,默認負載因子0.75
    }

3、Hashtable(int initialCapacity)

  該構造函數意在構造一個指定初始容量和默認負載因子(0.75f)的空 Hashtable,其源碼如下:

    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);    
    }

4、Hashtable(Map<? extends K, ? extends V> t)

  該構造函數意在構造一個與指定 Map 具有相同映射的 Hashtable,其初始容量不小於11(具體依賴於指定Map的大小),負載因子是0.75f,是 Java Collection Framework 規範推薦提供的,其源碼如下:

    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2 * t.size(), 11), 0.75f);
        putAll(t);
    }

與 HashMap 類似,構建一個 Hashtable 時也需要指定初始容量和負載因子這兩個非常重要的參數,它們是影響 Hashtable 性能的關鍵因素。其中,容量表示哈希表中桶的數量(table 數組的大小),初始容量是創建哈希表時桶的數量;負載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。

對於 Hashtable 而言,查找一個元素的平均時間是 O(1+a) (a 指的是鏈的長度,是一個常數)。特別地,負載因子越大,對空間的利用更充分,但查找效率的也就越低;若負載因子越小,那麼哈希表的數據將越稀疏,對空間造成的浪費也就越嚴重。系統默認負載因子爲0.75f,這是時間和空間成本上一種折衷,一般情況下我們是無需修改的。

2.3 Hashtable 的數據結構

我們知道,在 Java 中最常用的兩種結構是數組和鏈表,其中數組的特點是:尋址容易,插入和刪除困難;而鏈表的特點是:尋址困難,插入和刪除容易。Hashtable 和 HashMap 綜合了兩者的特性,是一種尋址容易、插入和刪除也容易的數據結構。實際上,Hashtable 和 HashMap 本質上都是一個鏈表數組。數組中存放的元素是 Entry 對象,而 Entry 對象是一種典型鏈狀結構,定義如下:

static class Entry<K,V> implements Map.Entry<K,V> {

    K key;     // 鍵值對的鍵
    V value;        // 鍵值對的值
    Entry<K,V> next;     // 指向下一個節點的指針
    int hash;     // key 的哈希值(與HashMap中key的哈希值計算方式不同)

    /**
     * Creates new entry.
     */
    protected Entry(int h, K k, V v, Entry<K,V> n) {     // Entry 的構造函數
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    ......
}

Entry 爲 Hashtable 的內部類,實現了 Map.Entry 接口,是個典型的四元組,包含了鍵 key、值 value、指向下一個節點的指針 next,以及 Key 的 hash 值四個屬性。事實上 Entry 是構成哈希表的基石,是哈希表所存儲的元素的具體形式。

2.4 Hashtable 的快速存取

我們知道,在 HashMap 中,最常用的兩個操作就是:put(Key,Value) 和 get(Key)。同樣地,這兩個操作也是 Hashtable 最常用的兩個操作。下面我們結合 JDK 源碼看 Hashtable 的存取實現。

1、Hashtable 的存儲實現 put(key, vlaue)

  在Hashtable中,鍵值對的存儲是也是通過 put(key, vlaue) 方法來實現的,不同於 HashMap 的是,其 put 操作是線程安全的,源碼如下:

    public synchronized V put(K key, V value) {     // 加鎖同步,保證Hashtable的線程安全性
        // Make sure the value is not null
        if (value == null) {      // 不同於HashMap,Hashtable不允許空的value
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry tab[] = table;
        int hash = key.hashCode();   // key 的哈希值,同時也暗示Hashtable不同於HashMap,其不允許空的key
        int index = (hash & 0x7FFFFFFF) % tab.length;   // 取餘計算節點存放桶位,0x7FFFFFFF 是最大的int型數的二進制表示
        // 先查找Hashtable上述桶位中是否包含具有相同Key的K/V對
        for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        // 向Hashtable中插入目標K/V對
        modCount++;     // 發生結構性改變,modCount加1
        if (count >= threshold) {    //在插入目標K/V對前,先檢查是否需要擴容(不同於HashMap的插入後檢查是否需要擴容) 
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            index = (hash & 0x7FFFFFFF) % tab.length;   // 擴容後,重新計算K/V對插入的桶位
        }

        // Creates the new entry.
        Entry<K, V> e = tab[index];
        tab[index] = new Entry<K, V>(hash, key, value, e); // 將K/V對鏈入對應桶中鏈表,併成爲頭結點
        count++;     // Hashtable中Entry數目加1
        return null;
    }

通過上述源碼我們可以看出,Hashtable 與 HashMap 保存數據的過程基本相同:首先,計算 key 的 hash 值並確定 K/V 對要插入的桶位;其次,查找該桶位中是否存在具有相同的 key 的 K/V 對,若存在則覆蓋直接對應的 value 值,否則將該節點(K/V)保存在桶中的鏈表的鏈頭位置(最先保存的元素放在鏈尾)。當然,若該桶位是空的,則直接保存。特別地,在一些細節上,Hashtable 與 HashMap 還是有一定的差別的:

  • Hashtable不同於HashMap,前者既不允許key爲null,也不允許value爲null;
  • HashMap中用於定位桶位的Key的hash的計算過程要比Hashtable複雜一點,沒有Hashtable如此簡單、直接;
  • 在HashMap的插入K/V對的過程中,總是先插入後檢查是否需要擴容;而Hashtable則是先檢查是否需要擴容後插入;
  • Hashtable不同於HashMap,前者的put操作是線程安全的。

2、Hashtable 的重哈希操作 rehash()

重哈希過程主要是一個重新計算原 Hashtable 中的元素在新 table 數組中的位置並進行復制處理的過程,我們直接看其源碼:

    protected void rehash() {
        int oldCapacity = table.length;   // 先獲取舊的Hashtable桶的數量,即容量
        Entry[] oldMap = table;

        int newCapacity = oldCapacity * 2 + 1;    // 擴容,擴到原始容量的2倍再加1
        Entry[] newMap = new Entry[newCapacity];   // 創建擴容後的新的鏈表數組

        modCount++;                // 重哈希操作是一個結構性改變操作,modCount加1
        threshold = (int) (newCapacity * loadFactor);   // 新的閾值   
        table = newMap;

        // 將原哈希表中的節點逐個複製到新的哈希表中
        for (int i = oldCapacity; i-- > 0;) {
            for (Entry<K, V> old = oldMap[i]; old != null;) {
                Entry<K, V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }

特別需要注意的是,在重哈希的過程中,原屬於同一個桶中的 Entry 對象可能會被分到不同的桶,因爲 Hashtable 的容量發生了變化,那麼(e.hash & 0x7FFFFFFF) % newCapacity 的值也會發生相應的變化。退一步說,如果重哈希後原屬於一個桶中的 Entry 對象仍屬於同一桶,那麼重哈希也就失去了意義。

3、Hashtable 的讀取實現

相對於 Hashtable 的存儲操作而言,讀取就顯得比較簡單了。因爲 Hashtable 只需通過 key 的 hash 值定位到 table 數組的某個特定的桶,然後查找並返回該 key 對應的 value 即可,源碼如下:

    public synchronized V get(Object key) {    // 不同於HashMap,Hashtable的讀取操作是同步的
        Entry tab[] = table;
        int hash = key.hashCode();   
        int index = (hash & 0x7FFFFFFF) % tab.length;   // 定位K/V對的桶位
        for (Entry<K, V> e = tab[index]; e != null; e = e.next) {   // 在特定桶中依次查找指定Key的K/V對
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;       
            }
        }
        return null;   // 查找失敗
    }

在這裏能夠根據 key 快速的取到 value,除了和 Hashtable 的數據結構密不可分外,還和 Entry 有莫大的關係。在前面就已經提到過,Hashtable 在存儲過程中並沒有將 key,value 分開來存儲,而是當做一個整體 Entry 對象(四元組)來處理的。可以看到,在 Entry 對象中,value 的地位要比 key 低一些,相當於是 key 的附屬。在讀取細節上,Hashtable 與 HashMap 的主要差別如下:

  • 不同於 HashMap,Hashtable 的讀取操作是同步的;
  • 在 HashMap 中,若讀取到的 Value 值爲 NULL,則存在如下兩種可能:該 key 對應的值就是 null 或者 HashMap 中不存在該 key;而在 Hashtable 中卻只有一種可能:Hashtable 中不存在含有該 key 的 Entry。造成這種差別的原因正是二者對 Key 和 Value 的限制的不同:HashMap 最多允許一個 Key 爲 null,但允許多個 value 值爲 null;Hashtable 既不允許空的 Key,也不允許空的 Value。

 

三. HashMap、Hashtable 與 ConcurrentHashMap 的聯繫與區別

3.1 Hashtable 與 HashMap 的聯繫與區別

  (1) HashMap 和 Hashtable 的實現模板不同:雖然二者都實現了 Map 接口,但 HashTable 繼承於 Dictionary 類,而 HashMap 是繼承於 AbstractMap。Dictionary 是是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的骨幹實現,它以最大限度地減少實現此接口所需的工作。

  (2) HashMap 和 Hashtable 對鍵值的限制不同:HashMap 可以允許存在一個爲 null 的 key 和任意個爲 null 的 value,但是 HashTable 中的 key 和 value 都不允許爲 null。

  (3) HashMap和Hashtable的線程安全性不同:Hashtable 的方法是同步的,實現線程安全的 Map;而 HashMap 的方法不是同步的,是Map的非線程安全實現。

  (4) HashMap 和 Hashtable 的地位不同:在併發環境下,Hashtable 雖然是線程安全的,但是我們一般不推薦使用它,因爲有比它更高效、更好的選擇 ConcurrentHashMap;而單線程環境下,HashMap 擁有比 Hashtable 更高的效率(Hashtable的操作都是同步的,導致效率低下),所以更沒必要選擇它了。

3.2 Hashtable 與 ConcurrentHashMap 的聯繫與區別

  Hashtable 和 ConcurrentHashMap 都可以用於併發環境,但是 Hashtable 的併發性能遠不如 ConcurrentHashMap,這種差異是由它們的底層實現決定的。我們知道 ConcurrentHashMap 引入了分段鎖機制,在默認理想狀態下,ConcurrentHashMap 可以支持16個線程執行併發寫操作及任意數量線程的讀操作;而 Hashtable 無論在讀的過程中還是寫的過程中都會鎖定整個 map,因此在併發效率上遠不如ConcurrentHashMap。此外,Hashtable 和 ConcurrentHashMap 對鍵值的限制相同,二者的 key 和 value 都不允許是 null。

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