Java集合之Hashtable原始碼解析

GeneralAndroid發表於2019-03-04

在進行Hashtable原始碼解析之前,我先扔出Hashtable與HashMap有哪些區別?
1.關於null,HashMap允許key和value都可以為null,而Hashtable則不接受key為null或value為null的鍵值對。
2.關於執行緒安全,HashMap是執行緒不安全的,Hashtable是執行緒安全的,因為Hashtable的許多操作函式都用synchronized修飾。
3.Hashtable與HashMap實現的介面一致,但Hashtable繼承Dictionary,而HashMap繼承自AbstractMap,即父類不同。
4.預設初始容量不同,擴容大小不同。HashMap的hash陣列的預設大小是16,而且一定是2 的指數,增加方式old2;Hashtable中hash陣列預設大小是11,增加的方式是old2+1。

之前簡要分析過HashMap的程式碼,關於HashMap可點選:Java集合之HashMap原始碼解析 .下面我們開始進行Hashtable的分析,先看程式碼清單1:

public class HashtableTest {
    public static void main(String [] args){
        Hashtable<String, String> table=new Hashtable<>();
        Hashtable<String, String> table1=new Hashtable<>(16);
        Hashtable<String, String> table2=new Hashtable<>(16, 0.75f);
        HashMap<String,String>  map=new HashMap<>();
        Hashtable<String,String> table3=new Hashtable<>(map);
        table.put("T1", "1");
        table.put("T2", "2");
        table.put(null, "3");
        System.out.println();
        System.out.println(table.toString());

    }
}複製程式碼

我們看到在建立物件上,和HashMap的使用方式相似,我們繼續看其內部的建構函式是怎麼樣的,如程式碼清單2:

 private transient Entry<?,?>[] table;//陣列
    private transient int count;//鍵值對的數量
    private int threshold;//閥值
    private float loadFactor;//載入因子
    private transient int modCount = 0;//修改次數
    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;
        table = new Entry[initialCapacity];//這裡是與HashMap的區別之一,HashMap中table
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        initHashSeedAsNeeded(initialCapacity);
    }

    public Hashtable(int initialCapacity) {//指定初始陣列長度
        this(initialCapacity, 0.75f);
    }

    public Hashtable() {//從這裡可以看出容量的預設值為16,載入因子為0.75f.
        this(11, 0.75f);
    }

    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }複製程式碼

Hashtable的建構函式主要對table陣列進行了初始化,這裡沒有什麼要拿出來詳細講的,我們還是看下put的流程,程式碼清單3:

public synchronized V put(K key, V value) {//這裡方法修飾符為synchronized,所以是執行緒安全的。
        if (value == null) {
            throw new NullPointerException();//value如果為Null,丟擲異常
        }
        Entry tab[] = table;
        int hash = hash(key);//hash裡面的程式碼是hashSeed^key.hashcode(),null.hashCode()會丟擲異常,所以這就解釋了Hashtable的key和value不能為null的原因。
        int index = (hash & 0x7FFFFFFF) % tab.length;//獲取陣列元素下標,先對hash值取正,然後取餘。
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;//修改次數。
        if (count >= threshold) {//鍵值對的總數大於其閥值
            rehash();//在rehash裡進行擴容處理

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }
private int hash(Object k) {
        // hashSeed will be zero if alternative hashing is disabled.
        return hashSeed ^ k.hashCode();//在1.8的版本中,hash就直接為k.hashCode了。
    }
protected void rehash() {
        int oldCapacity = table.length;
        Entry<K,V>[] oldMap = table;
        int newCapacity = (oldCapacity << 1) + 1;//擴容,如果預設值是11,則擴容之後,陣列的長度為23
        if (newCapacity - MAX_ARRAY_SIZE > 0) {//這裡的最大值和HashMap裡的最大值不同,這裡Max_ARRAY_SIZE的是因為有些虛擬機器實現會限制陣列的最大長度。
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<K,V>[] newMap = new Entry[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        boolean rehash = initHashSeedAsNeeded(newCapacity);

        table = newMap;
        for (int i = oldCapacity ; i-- > 0 ;) {//遷移鍵值對
            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                if (rehash) {
                    e.hash = hash(e.key);
                }
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }複製程式碼

通過程式碼清單2和3,我們可以看到Hashtable的資料結構和HashMap一樣是一個Entry的陣列,陣列元素是一個單連結串列,這裡展示出了一個很明顯的區別,即hash值沒有擾動處理。那怎麼解決衝突呢?Hashtable的索引求值公式其實是:
((hashSeed^k.hashCode())&0x7FFFFFFF)%newCapacity——> hash&0x7FFFFFFF%newCapacity。hash&0x7FFFFFF是為了保證正數,因為hashCode的值有可能為負值,這樣說有可能會有同學說,取正數直接用Math.abs不就行了。。。但是你確定所有情況下,abs都能保證輸出是正數嗎?來來舉個例子給你看看:

這裡寫圖片描述
這裡寫圖片描述

哈哈,你還能說什麼。hash&0x7FFFFFFF是為了避免負值的出現,對newCapacity求餘是為了使index在陣列索引範圍之內。看到這估計就有人問了那麼HashMap中的hash&(tab.leng-1)怎麼解釋呢?如果不太明白,還請大家仔細看上篇文章,這裡簡單說一下,hash&(tab.length-1)其實是對(hash&0x7FFFFFFF)%newCapacity的程式碼級優化,簡而言之就是位運算的效能是優於求餘運算的。

細心的讀者可能會發現HashMap與Hashtable的最大值是不一樣的,上篇文章我們說過HashMap的長度是2的指數值,而HashMap的陣列的最大長度是:1<<30=1073741824,這個值是int範圍內2的指數值的最大值,不信可以列印1<<31=-2147483648。關於Hashtable的陣列的最大值原始碼註釋中有說明,是指虛擬機器實現對array的長度有限制,如果大家糾結於這個最大值,why the max value is Integer.MAX_SIZE-8,請參考下面兩個連結:

stackoverflow.com/questions/3…
stackoverflow.com/questions/3…
說完了Hash與最大值的梗,我們就要來看看執行緒安全,Hashtable中的主要方法都加了synchronized關鍵字來修飾(沒有被顏色覆蓋的),如下:

這裡寫圖片描述
這裡寫圖片描述

前面我們已經分析過存資料的過程,現在我們一起來看看取的過程。

 public synchronized V get(Object key) {//沒有什麼特殊性,就是加了一個synchronized,就是根據index來遍歷索引處的單連結串列。
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }複製程式碼

鑑於Hashtable是歷史遺留的類,現在很少有人使用它,即使我們在對執行緒安全有要求的場景中,也是通過使用ConcurrentHashMap來解決,而不是使用Hashtable 。這裡可以簡要的說一下原因:Hashtable使用synchronized來實現執行緒安全,效率不高,而ConcurrentHashMap採用鎖分段技術來實現執行緒安全,大大提高了效率。在多執行緒環境中,當A執行緒訪問Hashtable的put方法時,其他執行緒是不能訪問諸如get,clear這些方法的,但是在ConcurrentHashMap中只要保證A執行緒與B執行緒不是持有一個段鎖,是可以A執行緒訪問put時其他執行緒同時訪問get操作。

最後我們說一下Hashtable的初始容量為什麼是11?Hashtable的擴容方式是:old*2+1,初始容量11,第一次擴容為23,第二次擴容為47,可以看到Hashtable的容量肯定是奇數,有一些更是為質數。到這裡就涉及到了雜湊演算法相關的知識了,這裡就不展開說雜湊演算法相關的內容了。Hashtable之所以初始容量為11(質數)和擴容方式保證為奇數,是為了雜湊得更均勻,也就是減少碰撞發生的機率。

轉載請註明出處:blog.csdn.net/android_jia…

相關文章