一文看懂Hashtable源碼以及與HashMap的區別

前言

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

Hashtable數組

注意是Hashtable不是HashTable(t爲小寫),這不是違背了駝峯定理了嘛?這還得從Hashtable的出生提及,Hashtable是在Java1.0的時候建立的,而集合的統一規範命名是在後來的Java2開始約定的,而當時又發佈了新的集合代替它,因此這個命名也一直使用到如今,因此Hashtable是一個過期的集合了,不推崇你們使用這個類,雖然說Hashtable是過期的了,咱們仍是有必要分析一下它,以便對Java集合框架有一個總體的認知。安全

首先Hashtable採用拉鍊法處理哈希衝突,是線程安全的,鍵值不容許爲null,而後Hashtable繼承自Dictionary,實現Map接口,Hashtable有幾個重要的成員變量table、count、threshold、loadFactor多線程

table:是一個Entry[]數據類型,而Entry實際是一個單鏈表框架

count:Hashtable的大小,即Hashtable中保存的鍵值對數量函數

threshold:Hashtable的閾值,用於判斷是否須要調整Hashtable的容量,threshold = 容量負載因子,threshold=11*0.75 取整即8this

loadFactor:用來實現快速失敗機制的spa

clipboard.png

構造函數線程

Hashtable有4個構造函數3d

//無參構造函數 默認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保存在下標爲0的Entry中,即(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 11 = 0

clipboard.png

執行第二個for循環後,變成了20保存在下標爲70的Entry中,由於Hashtable擴容了4次,分別是從容量爲默認的11->23->47->95->191,而後此時容量是191,因此(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 191 = 70

clipboard.png

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都會擴容,擴容後容量變化了,相同的哈希地址取到的數組下標也就不同。

相關文章
相關標籤/搜索