一篇文章帶你搞定HashTable

可苯發表於2020-10-27

在我們們開講原始碼之前,首先需要了解下什麼是雜湊表?

雜湊表(Hash table 又稱雜湊表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構.也就是說,它通過把關鍵碼值對映到表中的一個位置來訪問記錄,以加快查詢的速度.這個對映函式就叫做雜湊函式,存放記錄的陣列叫做雜湊表.              —— 百度百科

如圖:

在Java中HashTable以陣列+連結串列來實現,相對於HashMap來說要簡單得多.HashTable不同於HashMap,它內部不允許插入null值,同時它是執行緒安全的,所有的讀寫操作都進行了鎖保護,但也難以避免的對讀寫效率產生了較大影響.因此在日常開發中為保證執行緒安全一般建議使用ConcurrentHashMap.

為啥Java中Hashtable中的t要小寫? 這不符合駝峰命名規則啊

stack overflow

大意: Hashtable建立於Java1,而集合的統一命名規範是後來在Java2中建立的,而當時又釋出了新集合來代替它,再加上大量Java程式使用Hashtable類,考慮到相容問題不可能將Hashtable改為HashTable.同時Hashtable已經過時了,不建議在程式碼中使用.

原始碼分析

結構圖

繼承關係

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

Dictionary是JDK1.0裡引入的抽象類,用於儲存鍵/值對,作用和Map類似.注意Dictionary類已經過時了,在實際開發中,可以通過實現Map介面來完成儲存鍵值對的功能.

類中屬性

    /** 內部維護了一個 Entry 陣列 */
    private transient Entry<?,?>[] table;

    /** 雜湊表裡的元素數量 */
    private transient int count;

    /** 觸發擴容的閾值 */
    private int threshold;

    /** 載入因子 預設 0.75 */
    private float loadFactor;

    /** 記錄 涉及到結構變化的次數(offer/remove/clear等)  */
    private transient int modCount = 0;

    /** 版本號 */
    private static final long serialVersionUID = 1421746759512286392L;

table陣列裡存的Entry實際上是一個單向連結串列,雜湊表的鍵值對都是存在table裡的.

 private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
        
        ...
    }

建構函式

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

    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);

        //初始容量最小為1
        if (initialCapacity==0) initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

Hashtable的預設容量是11,loadFactor預設載入因子0.75.threshold
陣列容量 * loadFactor.

核心函式

新增函式

public synchronized V put(K key, V value) {
    if (value == null) {
        throw new NullPointerException();
    }
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();

    // &運算取正值,再取模計算位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    Entry<K,V> entry = (Entry<K,V>)tab[index];

    // 如果這個hash和key都已經存在了,就把原來的value替換掉
    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;
}

put函式通過關鍵字synchronized保證了執行緒安全.第一行就表明了Hashtable中value都不能為null.通過hash & 0x7FFFFFFF來規避掉負數,再進行分配位置.如果key經常存在了,則覆蓋舊值並返回舊值.核心通過addEntry來新增元素.

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;

        // 若當前容量已經大於 閾值 進行rehash擴容
        if (count >= threshold) {  // threshold = count * loadFlor
            // Rehash the table if the threshold is exceeded
            rehash();
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        Entry<K,V> e = (Entry<K,V>) tab[index];
        // 最新插入的 排在最前面
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

addEntry函式 先會判斷如果需要擴容 當前數量 >= 閾值,呼叫rehash進行擴容,否則連結串列疊加.

    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // 新容量為老容量的一倍+1,
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        // 新閾值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

rehash函式先會把容量擴大1倍+1,然後建立一個newMap,把oldMap裡的元素遍歷複製到新的newMap裡,這個過程是比較耗時的,同時此操作後陣列和連結串列裡元素的位置都會發生改變.

刪除函式

    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        Entry<K,V> e = (Entry<K,V>)tab[index];
        // 遍歷連結串列 e 當前節點, prev 上一個節點  next 下一個節點
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            // 效能優化 先 進行hash 判斷
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    // 若當前節點非頭結點
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

remove函式比較簡單,通過key定位在陣列中的位置,再遍歷連結串列刪除元素.

獲取函式

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

get函式和remove函式比較類似,都是先通過key定位在陣列中的位置,再迭代連結串列找到元素並直接返回.

迭代器

Hashtable裡迭代器使用的是比較老的Enumeration介面,其作用和Iterator類似,只提供了遍歷的功能.雖然Enumeration還未被廢棄,但現在程式碼裡已經很少使用了.本篇文章就不再講述了,有興趣的可以去翻翻程式碼~

public interface Enumeration<E> {
    /** 判斷是否還有元素  */
    boolean hasMoreElements();

    /** 如果還有元素則返回下一個元素,否則拋NoSuchElementException異常   */
    E nextElement();
}

結語

Hashtable整體是比較簡單的,其內部充斥著大量的遍歷操作,當資料量大的時候操作會非常耗時,非特殊情況下是用不到的.
一般來說,在日常開發中非併發場景推薦使用HashMap,併發場景下雖然可以用Hashtable,但是更推薦使用ConcurrentHashMap.
ConcurrentHashMap內部雖然也是使用Synchronized,但它是針對單個物件的鎖,相比於Hashtable裡鎖的粒度更細,效率更高.

相關文章