1. 程式人生 > >Java集合---HashTable底層原理

Java集合---HashTable底層原理

有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法儲存起來,然後通過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差不多,如果我們對HashMap比較瞭解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個後面會闡述。

1.定義

 HashTable在Java中的定義如下:

1.  publicclass Hashtable<K,V>    

2.  extends Dictionary<K,V>    

3.  implements Map<K,V>, Cloneable, java.io.Serializable  

從中可以看出HashTable繼承Dictionary類,實現Map介面。其中Dictionary類是任何可將鍵對映到相應值的類(如 Hashtable)的抽象父類。每個鍵和每個值都是一個物件。在任何一個 Dictionary 物件中,每個鍵至多與一個值相關聯。Map是"key-value鍵值對"介面。

    HashTable採用"拉鍊法"實現雜湊表,它定義了幾個重要的引數:table、count、threshold、loadFactor、modCount。

   table:為一個Entry[]陣列型別,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,雜湊表的"key-value鍵值對"都是儲存在Entry陣列中的。

   count:HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。

   threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*載入因子"。

   loadFactor:載入因子。

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

2.構造方法

HashTabel中存在5個建構函式。通過這5個建構函式我們構建出一個我想要的HashTable

 1.預設建構函式,容量為11,載入因子為0.75

1.  1.public Hashtable() {    

2.  2.        this(110.75f);    

3.  3.    }    

2.用指定初始容量和預設的載入因子 (0.75) 構造一個新的空雜湊表:

1.  1.public Hashtable(int initialCapacity) {    

2.  2.        this(initialCapacity, 0.75f);    

3.  3.    }    

3.用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化hashSeed引數,其中hashSeed用於計算keyhash值,它與keyhashCode進行按位異或運算。這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突:

1.  1.public Hashtable(int initialCapacity, float loadFactor) {    

2.  2.        //驗證初始容量

3.  3.        if (initialCapacity < 0)    

4.  4.            thrownew IllegalArgumentException("Illegal Capacity: "+    

5.  5.                                               initialCapacity);    

6.  6.        //驗證載入因子

7.  7.        if (loadFactor <= 0 || Float.isNaN(loadFactor))    

8.  8.            thrownew IllegalArgumentException("Illegal Load: "+loadFactor);    

9.  9.    

10. 10.        if (initialCapacity==0)    

11. 11.            initialCapacity = 1;    

12. 12.            

13. 13.        this.loadFactor = loadFactor;    

14. 14.            

15. 15.        //初始化table,獲得大小為initialCapacitytable陣列

16. 16.        table = new Entry[initialCapacity];    

17. 17.        //計算閥值

18. 18.        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);    

19. 19.        //初始化HashSeed

20. 20.        initHashSeedAsNeeded(initialCapacity);    

21. 21.    }    

22.

23.

24. 1.privateint hash(Object k) {    

25. 2.        return hashSeed ^ k.hashCode();    

26. 3.    }    

 4.構造一個與給定的 Map 具有相同對映關係的新雜湊表:

1.  1.public Hashtable(Map<? extends K, ? extends V> t) {      

2.  2.        //設定table容器大小,其值==t.size * 2 + 1    

3.  3.        this(Math.max(2*t.size(), 11), 0.75f);      

4.  4.        putAll(t);      

5.      }    

3.主要方法

   HashTableAPI對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這裡我只介紹兩個最根本的方法:putget

首先我們先看put方法:將指定 key 對映到此雜湊表中的指定 value。注意這裡鍵key和值value都不可為空。

1.  1.publicsynchronized V put(K key, V value) {    

2.  2.        // 確保value不為null  

3.  3.        if (value == null) {    

4.  4.            thrownew NullPointerException();    

5.  5.        }    

6.  6.    

7.  7.        /* 

8.  8.         * 確保keytable[]是不重複的

9.  9.         * 處理過程:

10. 10.         * 1、計算keyhash值,確認在table[]中的索引位置

11. 11.         * 2、迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值

12. 12.         */

13. 13.        Entry tab[] = table;    

14. 14.        int hash = hash(key);    //計算keyhash

15. 15.        int index = (hash & 0x7FFFFFFF) % tab.length;     //確認該key的索引位置

16. 16.        //迭代,尋找該key,替換

17. 17.        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {    

18. 18.            if ((e.hash == hash) && e.key.equals(key)) {    

19. 19.                V old = e.value;    

20. 20.                e.value = value;    

21. 21.                return old;    

22. 22.            }    

23. 23.        }    

24. 24.    

25. 25.        modCount++;    

26. 26.        if (count >= threshold) {  //如果容器中的元素數量已經達到閥值,則進行擴容操作

27. 27.            rehash();    

28. 28.            tab = table;    

29. 29.            hash = hash(key);    

30. 30.            index = (hash & 0x7FFFFFFF) % tab.length;    

31. 31.        }    

32. 32.    

33. 33.        // 在索引位置處插入一個新的節點

34. 34.        Entry<K,V> e = tab[index];    

35. 35.        tab[index] = new Entry<>(hash, key, value, e);    

36. 36.        //容器中元素+1  

37. 37.        count++;    

38. 38.        returnnull;    

39. 39.    }    

  put方法的整個處理流程是:計算keyhash值,根據hash值獲得keytable陣列中的索引位置,然後迭代該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 鏈的頭部

如下:

首先我們假設一個容量為5table,存在81013161721。他們在table中位置如下:

然後我們插入一個數:put(16,22)key=16table的索引位置為1,同時在1索引位置有兩個數,程式對該連結串列進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。

put(33,33)key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。

HashTabledput方法中有兩個地方需要注意:

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

1.  1.protectedvoid rehash() {    

2.  2.        int oldCapacity = table.length;    

3.  3.        //元素

4.  4.        Entry<K,V>[] oldMap = table;    

5.  5.    

6.  6.        //新容量=舊容量 * 2 + 1  

7.  7.        int newCapacity = (oldCapacity << 1) + 1;    

8.  8.        if (newCapacity - MAX_ARRAY_SIZE > 0) {    

9.  9.            if (oldCapacity == MAX_ARRAY_SIZE)    

10. 10.                return;    

11. 11.            newCapacity = MAX_ARRAY_SIZE;    

12. 12.        }    

13. 13.            

14. 14.        //新建一個size = newCapacity HashTable  

15. 15.        Entry<K,V>[] newMap = new Entry[];    

16. 16.    

17. 17.        modCount++;    

18. 18.        //重新計算閥值

19. 19.        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);    

20. 20.        //重新計算hashSeed  

21. 21.        boolean rehash = initHashSeedAsNeeded(newCapacity);    

22. 22.    

23. 23.        table = newMap;    

24. 24.        //將原來的元素拷貝到新的HashTable

25. 25.        for (int i = oldCapacity ; i-- > 0 ;) {    

26. 26.            for (Entry<K,V> old = oldMap[i] ; old != null ; ) {    

27. 27.                Entry<K,V> e = old;    

28. 28.                old = old.next;    

29. 29.    

30. 30.                if (rehash) {    

31. 31.                    e.hash = hash(e.key);    

32. 32.                }    

33. 33.                int index = (e.hash & 0x7FFFFFFF) % newCapacity;    

34. 34.                e.next = newMap[index];    

35. 35.                newMap[index] = e;    

36. 36.            }    

37. 37.        }    

38. 38.    }    

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

   2下面是計算key的hash值,這裡hashSeed發揮了作用。

1.  1.privateint hash(Object k) {    

2.  2.        return hashSeed ^ k.hashCode();    

3.      }   

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

1.  1.publicsynchronized V get(Object key) {    

2.  2.        Entry tab[] = table;    

3.  3.        int hash = hash(key);    

4.  4.        int index = (hash & 0x7FFFFFFF) % tab.length;    

5.  5.        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {    

6.  6.            if ((e.hash == hash) && e.key.equals(key)) {    

7.  7.                return e.value;    

8.  8.            }    

9.  9.        }    

10. 10.        returnnull;    

11. 11.    }    

4.HashMap的區別

1.HashMap可以允許存在一個為nullkey和任意個為nullvalue,但是HashTable中的keyvalue都不允許為null。如下:

HashMap遇到為nullkey時,它會呼叫putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是物件都可以。

1.  if (key == null)  

2.  return putForNullKey(value);  

而當HashTable遇到null時,他會直接丟擲NullPointerException異常資訊。

1.  if (value == null) {  

2.  thrownew NullPointerException();  

3.          }  

2.Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建議如果是涉及到多執行緒同步時採用HashTable,沒有涉及就採用HashMap

3.HashTable有一個contains(Objectvalue),功能和containsValue(Object value)功能一樣。

4.兩個遍歷方式的內部實現上不同。

HashtableHashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。

5.雜湊值的使用不同,HashTable直接使用物件的hashCode。而HashMap重新計算hash值。程式碼是這樣的: 
int hash = key.hashCode(); 
int index = (hash & 0x7FFFFFFF) % tab.length; 
HashMap重新計算hash值,而且用與代替求模: 
int hash = hash(k); 
int i = indexFor(hash, table.length); 

6.HashtableHashMap它們兩個內部實現方式的陣列的初始大小和擴容的方式。HashTablehash陣列預設大小是11,增加的方式是 old*2+1HashMaphash陣列的預設大小是16,而且一定是2的指數。