1. 程式人生 > >深入理解系列之JAVA資料結構(4)——Hashtable

深入理解系列之JAVA資料結構(4)——Hashtable

1、Hashtable和HashMap,從儲存結構和實現來講基本上都是相同的,
Hashtable繼承自Dictionary類,而HashMap繼承自AbstractMap類,但二者都實現了Map介面。
2、它和HashMap的最大的不同是它是執行緒安全的,另外它不允許key和value為null。
3、Hashtable是個過時的集合類,不建議在新程式碼中使用,不需要執行緒安全的場合可以用HashMap替換,需要執行緒安全的場合可以用ConcurrentHashMap替換!

因為HashMap和HashTable大部分原理都是相同的,所以我們首先做一個表來比較一下二者核心的概念,然後在重點講述幾個點!
這裡寫圖片描述

問題一、陣列索引位的確認有何不同?

我們在HashMap中已經講過,為了避免碰撞機率發生,Hashtable採用了素數作為實際的初始化容量的大小,為了保證擴容後的容量依然是素數,通過*2+1來實現!所以,不必有JDK8的高位運算來參與碰撞優化的機制,所以index = (hash & 0x7FFFFFFF) % tab.length;中的hash是直接的hashcode值:

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value
== null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; 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; }

我們知道0x7FFFFFFF = 0111 1111 1111 1111 1111 1111 1111 1111,所以拿hashcode&0x7FFFFFFF就是為了去掉符號位罷了!
從這個原始碼中還可以發現:
1、另外當出現衝突的時候,HashMap會使用連結串列+紅黑樹,但是在Hashtable中就只用連結串列了,所以效率會有下降!
2、另外可以看到,方法中加了鎖synchronized,這也是Hashtable執行緒安全的原因,也是其效率較低的另一個原因!

問題二、擴容機制有哪些不同?

Hashtable的擴容後會重新遍歷所有的節點,然後重新計算新容量下的hash桶索引位然後一個個放置或者連結,其中連結採用頭插法,這也就以為著連結串列資料在重新重新整理後會出現倒置現象!先看原始碼:

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

        // overflow-conscious code
        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;
            }
        }
    }

前面的和HashMap一樣就是擴容,從for迴圈開始進行重新重新整理(注意:如果看不懂,可以直接推演一遍)——

1、遍歷Hash桶陣列,且從尾索引位置開始
2、以此索引位置作為頭結點,遍歷該索引位下的所有節點;
····> 1、快取當前節點old->e,並準備下一節點old=old.next
····> 2、計算在新陣列下的新索引位置,首先把當前節點e(由old快取的)採用頭插法插入或者接入新索引位置上的資料(如果新位置資料為null,則e直接指向null,否則指向新位置節點)
····> 3、把當前節點直接放到新陣列的位置上,即覆蓋原來位置上的資料!(因為原來的資料已經由e.next連線起來了,所以不必擔心丟失,執行這幾步的結果就是頭插法,把新的頭節點放在心陣列索引位置上)!

為了便於理解,我引用網路上的一篇部落格來闡述:
假設了我們的hash演算法就是簡單的用key mod 一下表的大小(也就是陣列的長度)。其中的雜湊桶陣列table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。在mod 2以後都衝突在table[1]這裡了。這裡假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大於 table的實際大小時進行擴容。接下來的三個步驟是雜湊桶陣列 resize成4,然後所有的Node重新rehash的過程。
這裡寫圖片描述