五分鐘看懂Hashtable原始碼以及與HashMap的區別

薛8發表於2019-03-08
image

前言

上一篇我們認識了什麼是MapHash,瞭解了Hash處理雜湊衝突的幾種常用方法(拉鍊法、開放定址法),以及分析了JDK1.8版本的HashMap原始碼,對Java集合框架有了初步的認識,我們本篇繼續分析JDK1.8版本的Hashtable原始碼,最後比較HashMapHashtable的區別。

Hashtable

注意是Hashtable不是HashTable(t為小寫),這不是違背了駝峰定理了嘛?這還得從Hashtable的出生說起,Hashtable是在Java1.0的時候建立的,而集合的統一規範命名是在後來的Java2開始約定的,而當時又釋出了新的集合代替它,所以這個命名也一直使用到現在,所以Hashtable是一個過時的集合了,不推崇大家使用這個類,雖說Hashtable是過時的了,我們還是有必要分析一下它,以便對Java集合框架有一個整體的認知。
首先Hashtable採用拉鍊法處理雜湊衝突,是執行緒安全的,鍵值不允許為null,然後Hashtable繼承自Dictionary,實現Map介面,Hashtable有幾個重要的成員變數tablecountthresholdloadFactor

  • table:是一個Entry[]資料型別,而Entry實際是一個單連結串列
  • count:Hashtable的大小,即Hashtable中儲存的鍵值對數量
  • threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量,threshold = 容量負載因子,threshold=11*0.75 取整即8
  • loadFactor:用來實現快速失敗機制的
    五分鐘看懂Hashtable原始碼以及與HashMap的區別

建構函式

Hashtable有4個建構函式

//無參建構函式 預設Hashtable容量是11,預設負載因子是0.75
public Hashtable() {
    this(11, 0.75f);
}

//指定Hashtable容量,預設負載因子是0.75
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

//指定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;
    this.loadFactor = loadFactor;
    //new一個指定容量的Hashtable
    table = new Entry<?,?>[initialCapacity];
    //閾值threshold=容量*負載因子
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

//包含指定Map的建構函式
public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}
複製程式碼

這裡的Hashtable容量和HashMap的容量就有區別,Hashtable並不要求容量是2的冪次方,而HashMap要求容量是2的冪次方。負載因子則預設都是0.75。

put方法

put方法是同步的,即執行緒安全的,這點和HashMap不一樣,還有具體的put操作和HashMap也存在很大的差別,Hashtable插入的時候是插入到連結串列頭部,而HashMap是插入到連結串列尾部

//synchronized同步鎖,所以Hashtable是執行緒安全的
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    //如果值value為空,則丟擲異常 至於為什麼官方不允許為空,下面給出分析
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    //直接取key的hashCode()作為雜湊地址,這與HashMap的取hashCode()之後再進行hash()的結果作為雜湊地址 不一樣
    int hash = key.hashCode();
    //陣列下標=(雜湊地址 & 0x7FFFFFFF) % Hashtable容量,這與HashMap的陣列下標=雜湊地址 & (HashMap容量-1)計算陣列下標方式不一樣,前者是取模運算,後者是位於運算,這也就是為什麼HashMap的容量要是2的冪次方的原因,效率上後者的效率更高。
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //遍歷Entry連結串列,如果連結串列中存在key、雜湊地址相同的節點,則將值更新,返回舊值
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    //如果為新的節點,則呼叫addEntry()方法新增新的節點
    addEntry(hash, key, value, index);
    //插入成功返回null
    return null;
}

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

    Entry<?,?> tab[] = table;
    //如果當前鍵值對數量>=閾值,則執行rehash()方法擴容Hashtable的容量
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        //獲取key的hashCode();
        hash = key.hashCode();
        //重新計算下標,因為Hashtable已經擴容了。
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    //獲取當前Entry連結串列的引用 復賦值給e
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //建立新的Entry連結串列的 將新的節點插入到Entry連結串列的頭部,再指向之前的Entry,即在連結串列頭部插入節點,這個和HashMap在尾部插入不一樣。
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}
複製程式碼

hashCode()為什麼要& 0x7FFFFFFF呢?因為某些物件的hashCode()可能是負值,& 0x7FFFFFFF保證了進行%運算時候得到的下標是個正數

get方法

get方法也是同步的,和HashMap不一樣,即執行緒安全,具體的get操作和HashMap也有區別。

//同步
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    //和put方法一樣 都是直接獲取key的hashCode()作為雜湊地址
    int hash = key.hashCode();
    //和put方法一樣 通過(雜湊地址 & 0x7FFFFFFF)與Hashtable容量做%運算 計算出下標
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //遍歷Entry連結串列,如果連結串列中存在key、雜湊地址一樣的節點,則找到 返回該節點的值,否者返回null
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
複製程式碼

remove方法

//同步
public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    //遍歷Entry連結串列,e為當前節點,prev為上一個節點
    for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
        //找到key、雜湊地址一樣的節點
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            //如果上一個節點不為空(即不是當前節點頭結點),將上一個節點的next指向當前節點的next,即將當前節點移除連結串列
            if (prev != null) {
                prev.next = e.next;
            } else { //如果上一個節點為空,即當前節點為頭結點,將table陣列儲存的連結串列頭結點地址改成當前節點的下一個節點
                tab[index] = e.next;
            }
            //Hashtable的鍵值對數量-1
            count--;
            //獲取被刪除節點的值 並且返回
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}
複製程式碼

rehash方法

Hashtable的rehash方法和HashMap的resize方法一樣,是用來擴容雜湊表的,但是擴容的實現又有區別。

protected void rehash() {
    //獲取舊的Hashtable的容量
    int oldCapacity = table.length;
    //獲取舊的Hashtable引用,為舊雜湊表
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //新的Hashtable容量=舊的Hashtable容量 * 2 + 1,這裡和HashMap的擴容不一樣,HashMap是新的Hashtable容量=舊的Hashtable容量 * 2。
    int newCapacity = (oldCapacity << 1) + 1;
    //如果新的Hashtable容量大於允許的最大容量值(Integer的最大值 - 8)
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        //如果舊的容量等於允許的最大容量值則返回
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        //新的容量等於允許的最大容量值
        newCapacity = MAX_ARRAY_SIZE;
    }
    //new一個新的Hashtable 容量為新的容量
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    //計算新的閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    //擴容後遷移Hashtable的Entry連結串列到正確的下標上
    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;
        }
    }
}
複製程式碼

接下來我們執行以下程式碼,驗證以下資料遷移過程

Hashtable hashtable = new Hashtable();
for (int i = 1; i <= 24; i ++) {
    hashtable.put(String.valueOf(i), i);
}
for (int i = 25; i <= 80; i ++) {
    hashtable.put(String.valueOf(i), i);
}
複製程式碼

new一個Hashtable,預設容量是11,負載因子是0.75
執行第一個for迴圈後,20儲存在下標為0Entry中,即(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 11 = 0

五分鐘看懂Hashtable原始碼以及與HashMap的區別

執行第二個for迴圈後,變成了20儲存在下標為70Entry中,因為Hashtable擴容了4次,分別是從容量為預設的11->23->47->95->191,然後此時容量是191,所以(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 191 = 70

五分鐘看懂Hashtable原始碼以及與HashMap的區別

HashMap和Hashtable區別

到這裡我們分析了HashMap和Hashtable的原理,現在比較以下他們的區別。

不同點

  • 繼承的類不一樣:HashMap繼承的AbstractMap抽象類,Hashtable繼承的Dictionay抽象類
  • 應對多執行緒處理方式不一樣:HashMap是非執行緒安全的,Hashtable是執行緒安全的,所以Hashtable效率比較低
  • 定位演算法不一樣:HashMap通過key的hashCode()進行hash()得到雜湊地址,陣列下標=雜湊地址 & (容量 - 1),採用的是與運算,所以容量需要是2的冪次方結果才和取模運算結果一樣。而Hashtable則是:陣列下標=(key的hashCode() & 0x7FFFFFFF ) % 容量,採用的取模運算,所以容量沒要求
  • 鍵值對規則不一樣:HashMap允許鍵值為null,而Hashtable不允許鍵值為null
  • 雜湊表擴容演算法不一樣:HashMap的容量擴容按照原來的容量*2,而Hashtable的容量擴容按照原來的容量*2+1
  • 容量(capacity)預設值不一樣:HashMap的容量預設值為16,而Hashtable的預設值是11
  • put方法實現不一樣:HashMap是將節點插入到連結串列的尾部,而Hashtable是將節點插入到連結串列的頭部
  • 底層結構不一樣:HashMap採用了陣列+連結串列+紅黑樹,而Hashtable採用陣列+連結串列

為什麼HashMap允許null鍵值呢,而Hashtable不允許null鍵值呢?這裡還得先介紹一下什麼是null,我們知道Java語言中有兩種型別,一種是基本型別還有一種是引用型別,其實還有一種特殊的型別就是null型別,它不代表一個物件(Object)也不是一個物件(Object),然後在HashMap和Hashtable對鍵的操作中使用到了Object類中的equals方法,所以如果在Hashtable中置鍵值為null的話就可想而知會報錯了,但是為什麼HashMap可以呢?因為HashMap採用了特殊的方式,將null轉為了物件(Object),具體怎麼轉的,這裡就不深究了。

相同點

  • 實現相同的介面:HashMap和Hashtable均實現了Map介面
  • 負載因子(loadFactor)預設值一樣:HashMap和Hashtable的負載因子預設都是0.75
  • 採用相同的方法處理雜湊衝突:都是採用鏈地址法即拉鍊法處理雜湊衝突
  • 相同雜湊地址可能分配到不同的連結串列,同一個連結串列內節點的雜湊地址不一定相同:因為HashMap和Hashtable都會擴容,擴容後容量變化了,相同的雜湊地址取到的陣列下標也就不一樣。

參考

HashMap、HashTable、ConcurrentHashMap的原理與區別
Java集合之Hashtable原始碼解析

原文地址:https://ddnd.cn/2019/03/08/jdk1-8-hashtable/

相關文章