深度解析Hashtable

什麼是hashtable

HashTable一樣是基於哈希表實現的,其實相似HashMap,只不過有些區別,HashTable一樣每一個元素是一個key-value對,其內部也是經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。數組

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

HashTable 是線程安全的,能用於多線程環境中。Hashtable一樣也實現了Serializable接口,支持序列化,也實現了Cloneable接口,能被克隆。bash

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值,然ui

後經過hash值肯定在table數組中的索引位置,最後將value值替換或者插入新的元素,若是容器的數量達到閾值,就會進行擴充。this

源碼分析

構造方法

//默認構造函數,容量爲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索引位置處。spa

當程序試圖將一個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的,畢竟容量已經變了。:好比初始值十一、加載因子默認0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操做,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操做,一次類推。

相關文章
相關標籤/搜索