Java集合—Hashtable的源碼深度解析以及應用介紹

  本文基於JDK1.8對Java中的Hashtable集合的源碼進行了深度解析,包括各種方法、擴容機制、哈希算法、遍歷方法等方法的底層實現,最後給出了Hashtable和HashMap的詳細對比以及使用建議。

1 Hashtable的概述

public class Hashtable< K,V >
  extends Dictionary< K,V >
  implements Map< K,V >, Cloneable, Serializable

  Hashtable是來自於JDK1.0時代的古老key-value形式的集合類。類當中所有的方法都是同步的,數據安全的,效率低。
  JDK1.0的時候Hashtable是繼承的抽象類Dictionary,JDK1.2集合框架誕生之後,又實現了Map 接口,成爲了Java集合體系的一員。
  實現了Cloneable、Serializable標誌性接口,支持克隆、序列化操作。
  由於Map不屬於Collection集合體系,沒有實現Iterable接口,因此不支持獲取迭代器的方法iterator(),或者說Map的集合體系並沒有真正的迭代器。但是它們有自己的遍歷數據的方法。
  Hashtable的底層實際上是採用“拉鍊法”實現了一個哈希表,即使用一個數組作爲哈希表的骨架,每一個數組元素的位置稱爲“bucket”桶,桶裏存放的就是哈希值相同的鍵值對,如果一個桶裏面有多個鍵值對,那麼說明出現了哈希衝突,Hashtable使用“拉鍊法”解決衝突,每個桶的大小即該位置鏈表節點數量。Hashtable的key 和 value 都不允許爲null。
  關於數據結構中的哈希表,本文沒有多講,關於哈希表的詳細解釋在這篇文章中:數據結構—散列表(哈希表)的原理以及Java代碼的實現

2 Hashtable的源碼解析

2.1 主要類屬性

/**
 * 內部Entry[]數組,用來作爲哈希表的骨架,數組每一個Entry元素代表了一個鏈表的頭節點,Hashtable內部的哈希表的key-value鍵值對都是存儲在Entry節點中的。
 */
private transient Entry<?, ?>[] table;

/**
 * HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。
 */
private transient int count;

/**
 * Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*加載因子"。當count大於等於threshold時,需要調整容量(嘗試擴容)。
 */
private int threshold;

/**
 * 加載因子,是可以大於1的。
 */
private float loadFactor;

/**
 * 用來實現"fail-fast"機制的(也就是快速失敗)。
 */
private transient int modCount = 0;

  擴容閾值是由出初始容量和加載因子共同決定的,通常threshold=table.length*loadFactor,初始容量和加載因子越大,那麼就不需要頻繁的“擴容”,初始容量過大可能會浪費更多空間,加載因子越大會增加哈希衝突的風險,導致查找數據的時間過長。默認容量(11)和加載因子(0.75)在時間和空間成本上尋求一種折衷。
  關於modCount 的作用和fail-fast機制,早在ArrayLsit集合的源碼文章中就已經講解了,java.util包下的集合的fail-fast機制都是一樣的,這裏不再贅述,詳情可以看這篇文章:Java集合—ArrayList的源碼深度解析以及應用介紹

2.2 Entry節點

  Entry實際上就是Hashtable的一個內部類,作爲內部存儲key和value的容器,還保存key的hashCode值,同時由於Hashtable採用“拉鍊法”實現哈希表,每一個Entry還作爲鏈表的一個節點,因此內部還有一個到下一個節點的引用屬性。
  實際上Entry實現了Map.Entry接口,因此Entry內部還實現了相關方法共外部調用。EntrySet()方法返回的set集合的元素May.Entry,實際上就是返回的這個Entry節點的實例,後面會詳細講解!

private static class Entry<K,V> implements Map.Entry<K,V> {
    //哈希值,存儲起來方便後續使用,避免重複運算
    final int hash;
    //key
    final K key;
    //value
    V value;
    //下一個相同桶位的節點引用
    Entry<K,V> next;

    protected Entry(int hash, K key, V value, Entry<K,V> next) {
        this.hash = hash;
        this.key =  key;
        this.value = value;
        this.next = next;
    }

    @SuppressWarnings("unchecked")
    protected Object clone() {
        return new Entry<>(hash, key, value,
                (next==null ? null : (Entry<K,V>) next.clone()));
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

    public V setValue(V value) {
        if (value == null)
            throw new NullPointerException();

        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
                (value==null ? e.getValue()==null : value.equals(e.getValue()));
    }

    public int hashCode() {
        return hash ^ Objects.hashCode(value);
    }

    public String toString() {
        return key.toString()+"="+value.toString();
    }
}

  據此,我們能夠畫出Hashtable的大概數據結構圖:
在這裏插入圖片描述

2.3 構造器與初始化參數

2.3.1 Hashtable()

public Hashtable()

  構造一個新的,空的散列表,默認初始容量(11)和加載因子(0.75)。

public Hashtable() {
    //內部調用另外一個構造器,初始容量11,加載因子0.75
    this(11, 0.75f);
}

2.3.2 Hashtable(int initialCapacity)

public Hashtable(int initialCapacity)

  用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。

public Hashtable(int initialCapacity) {
    //內部調用另外一個構造器,用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。
    this(initialCapacity, 0.75f);
}

2.3.3 Hashtable(int initialCapacity, float loadFactor)

public Hashtable(int initialCapacity,float loadFactor)

  用指定初始容量和指定加載因子構造一個新的空哈希表。加載因子可以大於1,但是很明顯,加載因子越大,發生哈希衝突的概率也越大!
  這裏的initialCapacity也沒有要求是2的冪次方,但是HashMap 中初始化容量大小必須是 2 的冪次方。

/**
 * 建議數組最大容量,因爲某些VM實現可能需要部分長度用來存放數組頭部信息
 * 但是在HotSopt的虛擬機中,數組長度是可以超過這個限制的,可以達到Integer.MAX_VALUE – 2的長度
 * 並且在上面的源碼中能夠看到,我們分配的initialCapacity完全可以大於MAX_ARRAY_SIZE
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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);
    //如果初始容量爲0,則變成1
    if (initialCapacity == 0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    //創建數組
    table = new Entry<?, ?>[initialCapacity];
    //計算擴容閾值,取initialCapacity * loadFactor和MAX_ARRAY_SIZE + 1的最小值
    threshold = (int) Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

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

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

  構造一個與給定的 Map 具有相同映射關係的新哈希表。該哈希表是用足以容納給定 Map 中映射關係的初始容量和默認的加載因子(0.75)創建的。

public Hashtable(Map<? extends K, ? extends V> t) {
    //首先初始化hashtable
    this(Math.max(2*t.size(), 11), 0.75f);
    //底層調用putAll方法
    putAll(t);
}

2.4 put方法與擴容機制

public synchronized V put(K key, V value)

  將指定 key 映射到此哈希表中的指定 value。如果key或value爲 null,則拋出NullPointerException。
  put方法的源碼量比較多,並且是Hashtable中比較關鍵的一部分,但是並不難理解!我們分成三部分來講解:put、addEntry、rehash。

2.4.1 put

開放給外部調用的put方法主要可以分爲4步:

  1. 通過hash算法計算新鍵值對的位置;
  2. 判斷該位置是否存在元素,以及是否存在key相同的元素;
  3. 如果存在,並且找到相同的key,那麼替換value,返回舊值,方法結束;
  4. 如果不存在元素,或者沒有找到相同的key,那麼調用addEntry方法添加新元素節點,返回null,方法結束。
/**
 * 開放給外部調用的添加節點的方法,主要分4步:
 * 1、通過hash算法計算新鍵值對的位置
 * 2、判斷該位置是否存在元素,以及是否存在key相同的元素
 * 3、如果存在,並且找到相同的key,那麼替換value,返回舊值,方法結束
 * 4、如果不存在元素,或者沒有找到相同的key,那麼添加新元素節點,返回null,方法結束
 *
 * @param key   鍵
 * @param value 值
 * @return 舊值
 */
public synchronized V put(K key, V value) {
    /*1 通過hash算法計算新鍵值對所在位置*/
    // 確保value不爲null
    if (value == null) {
        throw new NullPointerException();
    }
    //獲取table數組
    Entry<?, ?> tab[] = table;
    /*Hashtable的hash算法*/
    //獲取key的hash值,該方法就是Object中的方法,key也可以重寫該方法,
    int hash = key.hashCode();
    //通過hash值進行相應的計算,確定key-value在table[]中存儲的索引位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //獲取數組在該索引位置的元素entry
    Entry<K, V> entry = (Entry<K, V>) tab[index];
    /*2 判斷該位置是否存在元素,以及是否存在key相同的元素
    如果entry不爲null,則說明有元素,並且可能不只有一個元素*/
    //那麼迭代index索引位置的鏈表,如果該位置處的鏈表中存在一個一樣的key,則替換其value,返回舊值
    for (; entry != null; entry = entry.next) {
        //判斷key相等的方案法,首先要求兩個key的hashCode()方法返回的hash值相等,然後要求兩個key的equals方法返回true。
        if ((entry.hash == hash) && entry.key.equals(key)) {
            //如果相等則替換舊的value並返回
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    //走到這一步,說明兩種情況,一種是entry爲null;另一種是entry不爲null,但是並沒有找到相同的key
    //此時說明需要添加節點,調用addEntry方法
    addEntry(hash, key, value, index);
    //舊值返回null
    return null;
}

  下面是關鍵源碼分析:
  首先是hashtable的哈希算法 ,從上面的源碼中我們知道,Hashtable的hash算法是:

(hash & 0x7FFFFFFF) % tab.length;

  其中hash就是key的hashCode方法的返回值,0x7FFFFFFF表示最大的int類型的數據,即2147483647,它的二進制表示就是除了首位符號位是 0,其餘都是1。
  由於hashCode方法返回的hash是int類型的整數,並可正可負,因此首先進行的hash & 0x7FFFFFFF的目的是將hash轉換爲一定是大於等於0的整數。
  然後再對底層數組的長度取餘,我們知道餘數一定會比除數更小,一個大於等於0的被除數除以一個正整數的餘數的範圍一定是[0, 除數)之間的,即[0, tab.length-1]。
  通過該hash算法計算出來的桶位置剛好能夠覆蓋整個數組的全部索引值,並且不會超出它的範圍!
  我們還能知道,由於最終桶位置是通過求餘“%”計算出來的,那麼如果被除數爲質數,即數組容量爲質數,此時求得的餘數將會更加均勻(hash函數爲什麼要選擇對素數求餘?),這也是後面的擴容算法(oldCapacity << 1) + 1的由來,加1之後新容量將變成質數,但是,hashtable並沒有強制保證容量一定是是質數,因爲可以通過構造器方式設置容量,這可能是HashTable已經不被sun公司推薦使用了。

   然後是判斷重複key,這裏的判斷方法是:

(entry.hash == hash) && entry.key.equals(key)

  即用了兩步,首先判斷兩個key的hashCode的值是否相等,然後判斷兩個key的equals方法是否返回true!
  最後如果不存在元素,或者沒有找到相同的key,那麼調用addEntry方法添加新元素節點,返回null,方法結束。
  下面來看addEntry方法源碼!

2.4.2 addEntry

添加新元素節點的方法addEntry又可以分爲3步:

  1. 判斷是否需要擴容;
  2. 如果需要擴容,那麼調用rehash方法擴容,並且重新計算新key在新數組的位置;
  3. 採用頭插法,插入節點,方法結束;
/**
 * 內部添加新節點的方法,主要分3步
 * 1、判斷是否需要擴容;
 * 2、如果需要擴容,那麼rehash()進行擴容,並且重新計算新key在行數組的位置;
 * 3、採用頭插法,插入節點;
 *
 * @param hash  hashcode方法獲取到的key的hash值
 * @param key   鍵
 * @param value 值
 * @param index Hashtable的hash算法計算出來的鍵值對存放的位置
 */
private void addEntry(int hash, K key, V value, int index) {
    //哈希表結構改變次數自增1,該值只與"fail-fast"機制有關
    modCount++;
    Entry<?, ?> tab[] = table;
    /*1 如果節點數量大於等於擴容閾值,此時開始擴容*/
    if (count >= threshold) {
        /*2 對於底層數組進行擴容以及內部的元素重新通過hash算法計算在新數組中的位置並移動到新數組中*/
        rehash();
        /*對於需要新增的k-v,同樣要重新計算在新數組中的位置*/
        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }
    /*3 創建新的entry節點,添加到鏈表頭部,使之成爲新的頭節點,即"頭插法"*/
    //獲取數組索引處的節點,該節點實際上就是鏈表的頭節點
    Entry<K, V> e = (Entry<K, V>) tab[index];
    //新建entry節點,next指向原來的該位置的頭節點,新節點放入數組在該索引的位置中,成爲新節點。
    tab[index] = new Entry<>(hash, key, value, e);
    //節點數量自增1
    count++;
}

   下面是關鍵源碼分析:
   首先是判斷是否需要擴容的源碼

count >= threshold

  這也就是threshold被稱爲擴容閾值的來源,元素數量大於等於該值就需要擴容。
  然後使用rehash()方法擴容,之後注意,由於經過了擴容,數組長度可能發生了變化,那麼還需要重新計算新節點的插入位置!
   最後就是插入新節點,這裏採用的“頭插法”。所謂的“頭插法”很簡單,實際上就是將新點作爲鏈表的頭節點插入,在Haashtable裏表示爲:新節點存入數組對應索引位置,原索引位置的節點成爲新節點的next節點!使用頭插法主要是考慮到新插入的數據,更可能作爲熱點數據被使用,放在頭部可以減少查找時間。
  在JDK1.8之前的HashMap也是採用“頭插法”插入元素節點,但是在JDK1.8時,改爲“尾插法”,因爲頭插法在多線程操作時可能形成環形鏈表造成死循環,具體原理在Hashmap原理的文章中會有講解,但是由於Hashtable是線程安全的,因此不需要改動!
  下面單獨來看看rehash()擴容方法!

2.4.3 rehash

擴容的方法rehash又可以分爲2步:

  1. 數組擴容,即嘗試建立一個更大的數組;
  2. 如果擴容成功,那麼循環遍歷舊的數組,轉移節點到新數組,方法結束;
/**
 * 內部數組擴容方法以及數據轉移機制,主要分兩步:
 * 1、數組擴容
 * 2、循環遍歷舊的數組,轉移節點
 */
protected void rehash() {
    /*1 數組擴容*/
    //獲取舊的容量
    int oldCapacity = table.length;
    //獲取舊的數組引用
    Entry<?, ?>[] oldMap = table;
    //新的容量爲 老的容量左移一位之後再加一,即oldCapacity*2+1
    int newCapacity = (oldCapacity << 1) + 1;
    //如果新容量減去MAX_ARRAY_SIZE大於0,這裏要注意:新容量並不一定大於MAX_ARRAY_SIZE,也可能是負數
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        //如果老的容量等於MAX_ARRAY_SIZE
        if (oldCapacity == MAX_ARRAY_SIZE)
            // 那麼繼續使用老的容量繼續運行,擴容結束,即達到了數組的最大容量,不再繼續擴容了
            return;
        //否則新容量直接等於MAX_ARRAY_SIZE
        newCapacity = MAX_ARRAY_SIZE;
    }
    //新建新容量的數組
    Entry<?, ?>[] newMap = new Entry<?, ?>[newCapacity];
    //數組結構改變次數加1
    modCount++;
    //計算新的擴容閾值
    threshold = (int) Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    //table指向新的數組
    table = newMap;

    /*2 循環遍歷舊數組,進行舊的數組元素節點的轉移*/
    for (int i = oldCapacity; i-- > 0; ) {
        //循環每一個數組節點處的鏈表,進行節點轉移操作
        for (Entry<K, V> old = (Entry<K, V>) oldMap[i]; old != null; ) {
            //獲取索引處的一個old節點,使用e來保存,第一次獲取的e就是該索引處的鏈表的頭節點
            Entry<K, V> e = old;
            //獲取old節點的下一個節點
            old = old.next;
            //計算老節點e在新數組中的索引位置
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            /*下面兩步,也是頭插法的方式插入元素*/
            //該節點的的下一個節點指向新數組的位置的頭節點
            e.next = (Entry<K, V>) newMap[index];
            //新數組的頭節點指向該節點
            newMap[index] = e;
        }
    }
}

  下面是關鍵源碼分析:
  首先是嘗試擴容的源碼:

int newCapacity = (oldCapacity << 1) + 1;

  上面的代碼用於計算新容量,新的容量爲老的容量左移一位之後再加一,這裏的<<是3運算符,能加快運算速度,用十進制表示即:newCapacity=oldCapacity*2+1,關鍵是下面一段代碼:

if (newCapacity - MAX_ARRAY_SIZE > 0)

  這段代碼用於判斷是否“真的能夠擴容以及是否需要重新分配新最大容量”,由於計算機的二進制運算法則,如果原本oldCapacity比較大,那麼新的容量可能會小於0,從而導致意想不到的情況。
  關於計算機二進制計算的坑可以看前面ArrayLsit的分析以及這篇文章:計算機進制轉換詳解以及Java的二進制的運算方法,在此不多贅述。
  因此嘗試擴容時,由於構造器中我們可以隨意設置初始容量,那麼根據oldCapacity的大小,可以分爲三種情況:

  1. oldCapacity 位於[1, Integer.MAX_VALUE /2-4]
      新容量newCapacity將爲正數,同時if中的判斷將爲假。那麼此時不會進入if代碼塊中,後續可以正常創建新數組進行擴容。 有趣的是,如果oldCapacity是Integer.MAX_VALUE /2-4,那麼newCapacity正好是MAX_ARRAY_SIZE,if語句中計算的值正好爲0。
  2. oldCapacity 位於[Integer.MAX_VALUE /2-3, Integer.MAX_VALUE -5]
      由於計算機二進制計算的法則,新容量newCapacity可能爲正也可能爲負,但是if中的判斷將一定爲真。那麼此時會進入if代碼塊中:如果oldCapacity等於MAX_ARRAY_SIZE,那麼不進行擴容;否則新容量newCapacity等於MAX_ARRAY_SIZE,這麼看起來,有可能是縮容而不是擴容(當oldCapacity大於MAX_ARRAY_SIZE的時候,新容量等於MAX_ARRAY_SIZE,即縮小了容量)。
      如果初始容量設置爲Integer.MAX_VALUE - 5,那麼在擴容時,newCapacity計算的結果爲-11,並且-11 - MAX_ARRAY_SIZE等於2147483646,大於0,此時將會以MAX_ARRAY_SIZE爲容量建立數組,我們知道MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)是小於Integer.MAX_VALUE – 5的,這就是“縮容”的由來!
  3. oldCapacity位於[Integer.MAX_VALUE -4, Integer.MAX_VALUE]
      新容量newCapacity還是負數,並且if中的判斷將爲假。那麼此時不會進入if代碼塊中,後續創建新數組進行擴容時將會拋出異常!
      如果初始容量設置爲Integer.MAX_VALUE -4,那麼在擴容時,newCapacity計算的結果爲-9,並且-9 - MAX_ARRAY_SIZE等於-2147483648,小於0,此時將會以-9爲容量建立新數組,導致NegativeArraySizeException異常。

   然後是數組節點轉移的部分:
  從舊的數組尾部開始循環每一個桶位中的鏈表的每一個節點,採用“頭插法”轉移到新的數組相應的位置上。
  這裏是從鏈表頭節點開始遍歷、轉移的,如果原來的鏈表中的節點在新數組中的位置還是一樣,那麼新數組中該鏈表節點的順序是原鏈表順序的倒序!

2.5 putAll方法

public synchronized void putAll(Map<? extends K, ? extends V> t)

  將指定映射的所有映射關係複製到此哈希表中,這些映射關係將替換此哈希表擁有的、針對當前指定映射中所有鍵的所有映射關係。如果指定的映射爲 null,則拋出NullPointerException。

public synchronized void putAll(Map<? extends K, ? extends V> t) {
    //內部處理方式非常簡單,就是循環遍歷參數集合,然後調用put方法一個個的添加節點
    for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
        put(e.getKey(), e.getValue());
}

2.6 remove方法

public synchronized V remove(Object key)

  從哈希表中移除該鍵及其相應的值。如果該鍵不在哈希表中,則此方法不執行任何操作。如果key爲 null,則拋出NullPointerException。
  remove方法比較簡單,就是首先計算出key的桶位置,然後循環該位置的鏈表,找出相同key的節點,移除該節點並返回value,沒找到就返回null。

public synchronized V remove(Object key) {
    Entry<?, ?> tab[] = table;
    //根據key定位到桶位置
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //獲取該位置的鏈表頭節點
    Entry<K, V> e = (Entry<K, V>) tab[index];
    //循環鏈表,查找key相同的節點,判斷是否相同是通過key的hashcode和equals方法一起比較得出來的結果
    //使用prev來保存e的前驅
    for (Entry<K, V> prev = null; e != null; prev = e, e = e.next) {
        /*如果key相同,那麼就算找到了*/
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            //如果前驅不爲null
            if (prev != null) {
                //那麼前驅的next節點指向e的next節點,刪除e節點
                prev.next = e.next;
            } else {
                //否則,e.next節點作爲頭節點,刪除e節點
                tab[index] = e.next;
            }
            //節點數量減少1
            count--;
            //返回e節點的value
            V oldValue = e.value;
            //value置空,助於GC回收
            e.value = null;
            return oldValue;
        }
    }
    //但這一步說明沒找到相同的key,返回null
    return null;
}

2.7 get方法

public synchronized V get(Object key)

  返回指定鍵所映射到的值,如果此映射不包含此鍵的映射,則返回 null。如果key爲 null,則拋出NullPointerException。
  get方法就更加簡單了,處理過程就是計算key的hash值,判斷在table數組中的索引位置,然後迭代鏈表,匹配直到找到相等的key返回返回value,若沒有找到返回null。

public synchronized V get(Object key) {
    Hashtable.Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Hashtable.Entry<?,?> e = tab[index]; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

2.8 clear方法

public synchronized void clear()

  清空哈希表。循環將數組索引位置置空即可,後續GC將會收集沒有引用到的鏈表。

public synchronized void clear() {
    Entry<?,?> tab[] = table;
    modCount++;
    //循環將數組索引位置置空即可,後續GC將會收集沒有引用到的鏈表
    for (int index = tab.length; --index >= 0; )
        tab[index] = null;
    //count置爲0
    count = 0;
}

2.9 遍歷的方法

  雖然Map體系下面的集合並沒有更加高級的迭代器(類似於liisiterator那種可以在迭代器中增刪改查數據的迭代器),但是他們也有自己的遍歷和設置值的方法。
  Hashtable共有四種遍歷的方法,三種是基於Map接口實現的:entrySet()、keySet()、values(),一種是誕生時自身就具備的:elements()、keys()。
  我們主要講解來自Map接口的遍歷方法,更古老的方法並不過多介紹:

public synchronized Enumeration elements()
  返回此哈希表中value的枚舉。
public synchronized Enumeration keys()
  返回此哈希表中的鍵的枚舉。

2.9.1 主要類屬性

  首次通過某些遍歷的方法請求結果視圖時,將會創建一個視圖對象,並賦值給下面對應的字段保存起來。因爲這些結果視圖和Map底層的哈希表的直接關聯的,對於哈希表的改變將會反映在結果視圖的遍歷中。因此後續調用相同的方法,直接返回已經生成的結果視圖即可,不需要創建新的視圖對象,非常的巧妙!

/*保存 keySet方法返回的結果視圖*/
private transient volatile Set<K> keySet;
/*保存 entrySet方法返回的結果視圖*/
private transient volatile Set<Map.Entry<K, V>> entrySet;
/*保存 values方法返回的結果視圖*/
private transient volatile Collection<V> values;

  下面的int類型常量主要是用於keySet、values、entrySet方法返回的視圖集合,在這個視圖集合獲取迭代器時,實際上內部調用同一個獲取迭代器的方法:getIterator(int type),返回的是同一個迭代器實現,主要是根據在構建迭代器時,傳入的迭代器類型進行判斷,並返回的不同的結果!具體判斷規則在“Map.Entry<K,V>接口”一節部分有詳解!

/*返回的迭代器集合類型*/
//keyset方法返回的set集合所使用的迭代器集合
private static final int KEYS = 0;
//values方法返回的Collection集合所使用的迭代器集合
private static final int VALUES = 1;
//entrySet方法返回的set集合所使用的迭代器集合
private static final int ENTRIES = 2;

2.9.2 entrySet方法

public synchronized Set<Map.Entry<K,V>> entrySet()

  返回此Map中包含的key-value鍵值對的set集合。該集合支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作。即此set支持元素移除,可從映射中移除相應的映射關係,但它不支持 add 或 addAll 操作。

2.9.2.1 entrySet方法

  先看看entrySet方法的源碼:

public Set<Map.Entry<K, V>> entrySet() {
    //判斷entrySet視圖是否爲null
    if (entrySet == null)
        //如果爲null則說明是第一次調用entrySet方法,那麼創建試圖對象並且賦值給entrySet字段
        entrySet = Collections.synchronizedSet(new EntrySet(), this);
    //返回entrySet視圖對象
    return entrySet;
}

  我們看到內部實際上調用的Collections集合工具類的synchronizedSet方法,該方法將會基於原集合進行包裝,並返回新的一個同步的SynchronizedSet類型的包裝集合,其操作元素的方法還是調用的傳入的原始集合的方法,這裏傳入的集合是一個EntrySet。

2.9.2.2 EntrySet內部類

  來看看EntrySet的源碼:

private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
    /**
     * 支持迭代器操作
     */
    public Iterator<Map.Entry<K, V>> iterator() {
        return getIterator(ENTRIES);
    }

    /**
     * 不支持 add 或 addAll 操作,因爲它的add方法是調用父類 AbstractSet的方法,而AbstractSet中add方法的實現是拋出異常
     */
    public boolean add(Map.Entry<K, V> o) {
        return super.add(o);
    }

    /**
     * 支持contains操作,實際上底層就是操作的table數組
     */
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> entry = (Map.Entry<?,?>)o;
        Object key = entry.getKey();
        Hashtable.Entry<?,?>[] tab = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;

        for (Hashtable.Entry<?,?> e = tab[index]; e != null; e = e.next)
            if (e.hash==hash && e.equals(entry))
                return true;
        return false;
    }

    /**
     * 支持remove操作,實際上底層就是操作的table數組
     */
    public boolean remove(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
        Object key = entry.getKey();
        Hashtable.Entry<?,?>[] tab = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;

        @SuppressWarnings("unchecked")
        Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>)tab[index];
        for(Hashtable.Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
            if (e.hash==hash && e.equals(entry)) {
                modCount++;
                if (prev != null)
                    prev.next = e.next;
                else
                    tab[index] = e.next;

                count--;
                e.value = null;
                return true;
            }
        }
        return false;
    }

    /**
     * 支持size操作
     *
     * @return
     */
    public int size() {
        return count;
    }

    /**
     * 支持clear操作
     */
    public void clear() {
        Hashtable.this.clear();
    }
}

  EntrySet表示Map的鍵值對對象(Map.Entry<K, V>)的set集合
  這裏的EntrySet實際上是一個Hashtable中的內部類,操作元素的方法,都是基於底層哈希表操作的。
  支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作,此 set 支持元素移除,可從映射中移除相應的映射關係。
  不支持 add 或 addAll 操作,因爲它的add方法是調用父類 AbstractSet的方法,而AbstractSet中add方法的實現是拋出異常。

2.9.2.3 synchronizedSet方法

  再來看看Collections.synchronizedSet的源碼:

/**
 * 返回一個同步set集合
 *
 * @param s     原集合
 * @param mutex 用來作爲鎖的對象
 * @return 同步的新集合, 是SynchronizedSet類型
 */
static <T> Set<T> synchronizedSet(Set<T> s, Object mutex) {
    return new SynchronizedSet<>(s, mutex);
}

/**
 * SynchronizedSet集合,的主要方法都是繼承SynchronizedCollection的方法
 */
static class SynchronizedSet<E> extends SynchronizedCollection<E> implements Set<E> {
    private static final long serialVersionUID = 487447009682186044L;

    SynchronizedSet(Set<E> s) {
        super(s);
    }

    SynchronizedSet(Set<E> s, Object mutex) {
        super(s, mutex);
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {
            return c.equals(o);
        }
    }

    public int hashCode() {
        synchronized (mutex) {
            return c.hashCode();
        }
    }
}

/**
 * SynchronizedCollection實現了同步集合的大部分方法,很容易就能看出來:
 * 它的同名方法全都是調用的傳入的集合(第一個參數)的方法,並且使用傳入的第二個參數作爲鎖對象,通過同步塊的方法來最終實現同步的
 * 實際上這就是Java設計模式——"裝飾設計模式"的應用
 */
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;
    //第一個參數,原集合
    final Collection<E> c;  // Backing Collection
    //第二個參數作爲鎖
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
        this.c = Objects.requireNonNull(c);
        this.mutex = Objects.requireNonNull(mutex);
    }

    /**
     * 裝飾加強後的方法
     *
     * @return
     */
    public int size() {
        //同步塊
        synchronized (mutex) {
            //調用被裝飾集合的方法
            return c.size();
        }
    }

    public boolean isEmpty() {
        synchronized (mutex) {
            return c.isEmpty();
        }
    }

    public boolean contains(Object o) {
        synchronized (mutex) {
            return c.contains(o);
        }
    }
    //…………
}

  從源碼能看出來,實際上Collections.synchronizedSet方法,就是一個裝飾設計模式的方法,傳入一個EntrySet對象和this對象,然後返回一個SynchronizedSet對象,該對象內部保存了傳入的兩個參數,它的同名方法,底層還是調用EntrySet對象的同名方法,並且使用this對象作爲鎖,這樣就完成了對EntrySet對象方法的裝飾加強,實現了同步!

2.9.2.4 Map.Entry<K,V>接口

  我們看到返回的set集合的元素是Map.Entry<K,V>類型,實際上該類型就是表示集合中的映射項(鍵-值對)。一個Map.Entry對象就表示一個鍵值對,那麼這個鍵值對和Hashtable中的節點對象Entry有什麼聯繫嗎?
  實際上,Map.Entry追溯到最頂層,它出現在Map接口,Entry作爲Map接口的內部接口,現在我們猜也能猜出來,這個Entry接口實際上是作爲Map集合體系中的節點的超級接口,Map的具體實現類的內部節點類均需要實現該Map.Entry接口。
  到這裏我們就是知道了,我們獲取的Map.Entry對象實際上返回的是各個Map實現類的節點對象,在Hashtable中我們獲取的是Entry節點(Entry內部類實現了Map.Entry接口),在HashMap中我們獲取的是Node節點(Node內部類也實現了Map.Entry接口)……
  Map.Entry接口提供的方法如下,實際上在前面的Entry節點內部類的介紹中已經說了,這些方法也是Entry節點的方法:

boolean equals(Object o) 比較指定對象與此項的相等性。
K getKey() 返回與此項對應的鍵。
V getValue() 返回與此項對應的值。
int hashCode() 返回此映射項的哈希碼值。
V setValue(V value) 用指定的值替換與此項對應的值(可選操作)。

  Map.Entry是通的entrySet()方法獲取的set集合的iterator(int type)方法獲取到的。EntrySet的iterator方法實現如下:

public Iterator<Map.Entry<K,V>> iterator() {
    return getIterator(ENTRIES);
}

  我們可以看到傳入的類型是ENTRIES類型,那麼該迭代器獲取的元素類型將會是一個Entry。
  實際上內部的Enumerator迭代器內部會根據傳入的類型返回的不同的元素:

return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);

  可以看到,如果是KEYS類型,那麼返回entry節點的key;如果是VALUES類型那麼就返回entry節點的value;否則,那就是ENTRIES,那麼就直接返回entry節點。

2.9.3 keySet方法

public synchronized Set keySet()

  返回此哈希表中key的set集合。
  查看keySet的源碼,可以發現和EntrySet的源碼非常相似:

public Set<K> keySet() {
    if (keySet == null)
        keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

  同樣採用了裝飾設計模式,只不過這裏的被裝飾的類變成了KeySet類。同樣該集合支持iterator、Iterator.remove、Set.remove、removeAll、 retainAll、和 clear 操作。即此set支持元素移除,可從映射中移除相應的映射關係,但它不支持 add 或 addAll 操作。

private class KeySet extends AbstractSet<K> {
    /**
     * KeySet集合迭代器的獲取,可以看到傳入的KEYS類型
     */
    public Iterator<K> iterator() {
        return getIterator(KEYS);
    }

    /**
     * 支持size操作
     */
    public int size() {
        return count;
    }
    /**
     * 支持contains操作,實際上是調用了外部類Hashtable的containsKey方法
     */
    public boolean contains(Object o) {
        return containsKey(o);
    }
    /**
     * 支持remove操作,實際上是調用了外部類Hashtable的remove方法
     * 前面添加的Hashtable.this.前綴是爲了引導調用外部類的同名方法
     */
    public boolean remove(Object o) {
        return Hashtable.this.remove(o) != null;
    }

    /**
     * 支持clear操作,實際上是調用了外部類Hashtable的clear方法
     * 前面添加的Hashtable.this.前綴是爲了引導調用外部類的同名方法
     */
    public void clear() {
        Hashtable.this.clear();
    }
}

2.9.4 values方法

public synchronized Collection values()

  返回此哈希表中value的 Collection 集合。
  查看values的源碼,可以發現和EntrySet、keySet的源碼非常相似:

public Collection<V> values() {
    if (values==null)
        values = Collections.synchronizedCollection(new ValueCollection(), this);
    return values;
}

  同樣採用了裝飾設計模式,只不過這裏的被裝飾的類變成了ValueCollection類。同樣該集合支持iterator、Iterator.remove、Collection.remove、removeAll、 retainAll、和 clear 操作。即此Collection支持元素移除,可從映射中移除相應的映射關係,但它不支持 add 或 addAll 操作。
  注意由於Map中的value是可能相等的,因此這裏的Collection.remove方法移除的是找到的第一個相等value的鍵值對。

3 HashMap 和 Hashtable的異同與應用

3.1 基於JDK1.8的HashMap 和 Hashtable的異同

相同點:

  1. 都是Map接口實現類,屬於Map體系的集合,都可以存放鍵值對,都屬於哈希表的實現,存放的鍵值對都是無序的。

不同點:

  1. 總體情況: HashMap 是JDK1.2 新添加的類,類當中方法的所有實現都是異步的,數據不安全,效率高。Hashtable是JDK1.0固有的類,類當中所有方法的實現都是同步的,數據安全的,效率低;
  2. 是否允許null: HashMap 的key和value允許 null; Hashtable 的key和value不允許 null;
  3. 遍歷方式: hashMap具有Map接口的3種遍歷方式keySet()、values()、entrySet(),Hashtable除了具有具有Map接口的3種遍歷方式之外,還有自己的keys()、elements()方法可以遍歷Map;
  4. 初始容量: HashMap 的默認初始容量爲11;Hashtable的默認初始容量爲16。
  5. 哈希算法: HashMap的哈希算法是對key的hash值進行了擾動運算(JDK1.8是1次位運算 + 1次異或運算),然後用結果和(容量-1)做&運算。Hashtable的哈希算法是對key的hash值和int最大值(0x7FFFFFFF)進行&運算,然後用結果對容量求餘%,並沒有擾動運算,因此HashMap的元素分佈更加均勻(擾動算法能夠讓哈希值分部的更加規律)。
  6. 擴容增量: Hashtable擴容之後的容量是原容量的兩倍加1。HashMap擴容之後的容量是原容量的兩倍。HashMap的容量要求必須爲2的冪次方,無論是初始容量還是擴容後的容量,Hashtable的初始化容量則沒有要求。
  7. 數據結構: JDK1.8中HashMap使用了數組+鏈表+紅黑樹的數據結構實現哈希表,而Hashtable而是使用了數組+鏈表的數據結構實現哈希表,HashMap的實現更加複雜,但是查找效率更高。
  8. 插入節點方式: JDK1.8中HashMap使用“尾插法“插入新節點,而Hashtable使用“頭插法”插入新節點。實際上在JDK1.8之前的HashMap也是採用頭插法插入元素節點,但是在JDK1.8時,改爲尾插法,因爲頭插法在多線程操作時可能形成環形鏈表造成死循環,但是由於Hashtable是線程安全的,因此不需要改動!

3.2 HashMap 和 Hashtable的應用

  在單線程環境下,推薦使用HashMap,因爲沒有同步,以及底層數據結構更加先進,速度更快;在併發環境下,不能使用HashMap,但是也不推薦使用Hashtable,因爲HashTable鎖住的是整個方法,鎖粒度太大,只有一把鎖,嚴重影響性能,推薦使用JUC包下面的ConcurrentHashMap,它採用Lock和CAS機制,降低鎖粒度,具有多把鎖,提升了併發量!

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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