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(11, 0.75f);
3. 3. }
2.用指定初始容量和預設的載入因子 (0.75) 構造一個新的空雜湊表:
1. 1.public Hashtable(int initialCapacity) {
2. 2. this(initialCapacity, 0.75f);
3. 3. }
3.用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個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,獲得大小為initialCapacity的table陣列
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.主要方法
HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這裡我只介紹兩個最根本的方法:put、get。
首先我們先看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. * 確保key在table[]是不重複的
9. 9. * 處理過程:
10. 10. * 1、計算key的hash值,確認在table[]中的索引位置
11. 11. * 2、迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值
12. 12. */
13. 13. Entry tab[] = table;
14. 14. int hash = hash(key); //計算key的hash值
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方法的整個處理流程是:計算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 鏈的頭部
如下:
首先我們假設一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:
然後我們插入一個數:put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數,程式對該“連結串列”進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。
在put(33,33),key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。
在HashTabled的put方法中有兩個地方需要注意:
1、HashTable的擴容操作,在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方法就會比較簡單,處理過程就是計算key的hash值,判斷在table陣列中的索引位置,然後迭代連結串列,匹配直到找到相對應key的value,若沒有找到返回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可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:
當HashMap遇到為null的key時,它會呼叫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.兩個遍歷方式的內部實現上不同。
Hashtable、HashMap都使用了 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.Hashtable和HashMap它們兩個內部實現方式的陣列的初始大小和擴容的方式。HashTable中hash陣列預設大小是11,增加的方式是 old*2+1。HashMap中hash陣列的預設大小是16,而且一定是2的指數。