深度解析Hashtable

Fysddsw_lc發表於2018-04-11

什麼是hashtable

HashTable同樣是基於雜湊表實現的,其實類似HashMap,只不過有些區別,HashTable同樣每個元素是一個key-value對,其內部也是通過單連結串列解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。

HashTable比較古老, 是JDK1.0就引入的類,而HashMap 是 1.2 引進的 Map 的一個實現。

HashTable 是執行緒安全的,能用於多執行緒環境中。Hashtable同樣也實現了Serializable介面,支援序列化,也實現了Cloneable介面,能被克隆。

Hashtable成員變數

private transient Entry[] table;  
// Hashtable中元素的實際數量  
private transient int count;  
// 閾值,用於判斷是否需要調整Hashtable的容量(threshold = 容量*載入因子)  
private int threshold;  
// 載入因子  
private float loadFactor;  
// Hashtable被改變的次數  
private transient int modCount = 0;  
複製程式碼

table是一個Entry[]陣列型別,而Entry實際上就是一個單向連結串列。雜湊表的"key-value鍵值對"都是儲存在Entry陣列中的。 

count是Hashtable的儲存大小,是Hashtable儲存的鍵值對的數量。

threshold是Hashtable臨界值,也叫閥值,如果Hashtable到達了臨界值,需要重新分配大小。閥值 = 當前陣列長度✖負載因子。預設的Hashtable中table的大小為11,負載因子的預設值為0.75。

loadFactor是負載因子, 預設為75%。

modCount指的是Hashtable被修改或者刪除的次數總數。用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他執行緒對其進行結構性的修改,這時迭代器會立馬感知到,並且立即丟擲ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)。

Hashtable的基本原理

從下面的程式碼中我們可以看出,Hashtable中的key和value是不允許為空的,當我們想要想Hashtable中新增元素的時候,首先計算key的hash值,然

後通過hash值確定在table陣列中的索引位置,最後將value值替換或者插入新的元素,如果容器的數量達到閾值,就會進行擴充。

原始碼分析

構造方法

    //預設建構函式,容量為11,負載因子是0.75
    public Hashtable() {
        this(11, 0.75f);
    }
    //用指定初始容量和預設的載入印在(0.74)構造一個空的雜湊表。
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
    //用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化
    hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。
    這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突:

    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,獲得大小為initialCapacity的table陣列  
    //這裡是與HashMap的區別之一,HashMap中table
        table = new Entry[initialCapacity];
    //計算閥值    
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    //初始化HashSeed值   
     initHashSeedAsNeeded(initialCapacity);
    }

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

put方法

public synchronized V put(K key, V value) {//這裡方法修飾符為synchronized,所以是執行緒安全的。
        // 確保value不為null  
        if (value == null) {
            throw new NullPointerException();//value如果為Null,丟擲異常
        }
        Entry tab[] = table;

        //計算key的hash值,確認在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)) {
                //迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值  
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;//修改次數。
        if (count >= threshold) {//鍵值對的總數大於其閥值
            rehash();//在rehash裡進行擴容處理
            tab = table;
            hash = hash(key);
            //hash&0x7FFFFFFF是為了避免負值的出現,對newCapacity求餘是為了使index
            在陣列索引範圍之內
            index = (hash & 0x7FFFFFFF) % tab.length;
        }
        //在索引出插入一個新的節點
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        //容器中元素+1  ;  
        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了。
    }
複製程式碼

 put方法的流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。

當程式試圖將一個key-value對放入HashMap中時,程式首先根據該 key的 hashCode() 返回值決定該 Entry 的儲存位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的儲存位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新新增的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新新增的 Entry 位於 Entry 鏈的頭部 

get方法

 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;
    }
複製程式碼

 相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table陣列中的索引位置,然後迭代連結串列,匹配直到找到相對應key的value,若沒有找到返回null。、

rehash方法

HashTable的擴容操作,在put方法中,如果需要向table[]中新增Entry元素,會首先進行容量校驗,如果容量已經達到了閥值,HashTable就會進行擴容處理rehash()

protected void rehash() {  
        int oldCapacity = table.length;  
        //元素  
        Entry<K,V>[] oldMap = table;  
  
        //新容量=舊容量 * 2 + 1  
        int newCapacity = (oldCapacity << 1) + 1;  
        if (newCapacity - MAX_ARRAY_SIZE > 0) { 
        //這裡的最大值和HashMap裡的最大值不同,這裡Max_ARRAY_SIZE的是
        因為有些虛擬機器實現會限制陣列的最大長度。 
            if (oldCapacity == MAX_ARRAY_SIZE)  
                return;  
            newCapacity = MAX_ARRAY_SIZE;  
        }  
          
        //新建一個size = newCapacity 的HashTable  
        Entry<K,V>[] newMap = new Entry[];  
  
        modCount++;  
        //重新計算閥值  
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);  
        //重新計算hashSeed  
        boolean rehash = initHashSeedAsNeeded(newCapacity);  
  
        table = newMap;  
        //將原來的元素拷貝到新的HashTable中  
        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;  
            }  
        }  
    }  複製程式碼

 在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經變了。:比如初始值11、載入因子預設0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,一次類推。

相關文章