在java中與有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法保存起來,然后通過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差不多,如果我們對HashMap比較了解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個后面會闡述。
一、定義
HashTable在Java中的定義如下:
public class Hashtable<K,V> extends Dictionary<K,V> 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異常,而不是等到迭代完成之后才告訴你(你已經出錯了)。
二、構造方法
在HashTabel中存在5個構造函數。通過這5個構造函數我們構建出一個我想要的HashTable。
public Hashtable() { this(11, 0.75f); }
默認構造函數,容量為11,加載因子為0.75。
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表。
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);</span><span style="color: #0000ff">if</span> (initialCapacity==0<span style="color: #000000">) initialCapacity </span>= 1<span style="color: #000000">; </span><span style="color: #0000ff">this</span>.loadFactor =<span style="color: #000000"> loadFactor; </span><span style="color: #008000">//</span><span style="color: #008000">初始化table,獲得大小為initialCapacity的table數組</span> table = <span style="color: #0000ff">new</span><span style="color: #000000"> Entry[initialCapacity]; </span><span style="color: #008000">//</span><span style="color: #008000">計算閥值</span> threshold = (<span style="color: #0000ff">int</span>)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1<span style="color: #000000">); </span><span style="color: #008000">//</span><span style="color: #008000">初始化HashSeed值</span>
initHashSeedAsNeeded(initialCapacity);
}
用指定初始容量和指定加載因子構造一個新的空哈希表。其中initHashSeedAsNeeded方法用於初始化hashSeed參數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與實例相關的隨機值,主要用於解決hash沖突。
private int hash(Object k) { return hashSeed ^ k.hashCode(); }
構造一個與給定的 Map 具有相同映射關系的新哈希表。
public Hashtable(Map<? extends K, ? extends V> t) { //設置table容器大小,其值==t.size * 2 + 1 this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }
三、主要方法
HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這里我只介紹兩個最根本的方法:put、get。
首先我們先看put方法:將指定 key
映射到此哈希表中的指定 value
。注意這里鍵key和值value都不可為空。
public synchronized V put(K key, V value) { // 確保value不為null if (value == null) { throw new NullPointerException(); }</span><span style="color: #008000">/*</span><span style="color: #008000"> * 確保key在table[]是不重復的 * 處理過程: * 1、計算key的hash值,確認在table[]中的索引位置 * 2、迭代index索引位置,如果該位置處的鏈表中存在一個一樣的key,則替換其value,返回舊值 </span><span style="color: #008000">*/</span><span style="color: #000000"> Entry tab[] </span>=<span style="color: #000000"> table; </span><span style="color: #0000ff">int</span> hash = hash(key); <span style="color: #008000">//</span><span style="color: #008000">計算key的hash值</span> <span style="color: #0000ff">int</span> index = (hash & 0x7FFFFFFF) % tab.length; <span style="color: #008000">//</span><span style="color: #008000">確認該key的索引位置 </span><span style="color: #008000">//</span><span style="color: #008000">迭代,尋找該key,替換</span> <span style="color: #0000ff">for</span> (Entry<K,V> e = tab[index] ; e != <span style="color: #0000ff">null</span> ; e =<span style="color: #000000"> e.next) { </span><span style="color: #0000ff">if</span> ((e.hash == hash) &&<span style="color: #000000"> e.key.equals(key)) { V old </span>=<span style="color: #000000"> e.value; e.value </span>=<span style="color: #000000"> value; </span><span style="color: #0000ff">return</span><span style="color: #000000"> old; } } modCount</span>++<span style="color: #000000">; </span><span style="color: #0000ff">if</span> (count >= threshold) { <span style="color: #008000">//</span><span style="color: #008000">如果容器中的元素數量已經達到閥值,則進行擴容操作</span>
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}</span><span style="color: #008000">//</span><span style="color: #008000"> 在索引位置處插入一個新的節點</span> Entry<K,V> e =<span style="color: #000000"> tab[index]; tab[index] </span>= <span style="color: #0000ff">new</span> Entry<><span style="color: #000000">(hash, key, value, e); </span><span style="color: #008000">//</span><span style="color: #008000">容器中元素+1</span> count++<span style="color: #000000">; </span><span style="color: #0000ff">return</span> <span style="color: #0000ff">null</span><span style="color: #000000">; }</span></pre>
put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table數組中的索引位置,然后迭代該key處的Entry鏈表(我們暫且理解為鏈表),若該鏈表中存在一個這個的key對象,那么就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。如下:
首先我們假設一個容量為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(),如下:
protected void rehash() { int oldCapacity = table.length; //元素 Entry<K,V>[] oldMap = table;</span><span style="color: #008000">//</span><span style="color: #008000">新容量=舊容量 * 2 + 1</span> <span style="color: #0000ff">int</span> newCapacity = (oldCapacity << 1) + 1<span style="color: #000000">; </span><span style="color: #0000ff">if</span> (newCapacity - MAX_ARRAY_SIZE > 0<span style="color: #000000">) { </span><span style="color: #0000ff">if</span> (oldCapacity ==<span style="color: #000000"> MAX_ARRAY_SIZE) </span><span style="color: #0000ff">return</span><span style="color: #000000">; newCapacity </span>=<span style="color: #000000"> MAX_ARRAY_SIZE; } </span><span style="color: #008000">//</span><span style="color: #008000">新建一個size = newCapacity 的HashTable</span> Entry<K,V>[] newMap = <span style="color: #0000ff">new</span><span style="color: #000000"> Entry[]; modCount</span>++<span style="color: #000000">; </span><span style="color: #008000">//</span><span style="color: #008000">重新計算閥值</span> threshold = (<span style="color: #0000ff">int</span>)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1<span style="color: #000000">); </span><span style="color: #008000">//</span><span style="color: #008000">重新計算hashSeed</span> <span style="color: #0000ff">boolean</span> rehash =<span style="color: #000000"> initHashSeedAsNeeded(newCapacity); table </span>=<span style="color: #000000"> newMap; </span><span style="color: #008000">//</span><span style="color: #008000">將原來的元素拷貝到新的HashTable中</span> <span style="color: #0000ff">for</span> (<span style="color: #0000ff">int</span> i = oldCapacity ; i-- > 0<span style="color: #000000"> ;) { </span><span style="color: #0000ff">for</span> (Entry<K,V> old = oldMap[i] ; old != <span style="color: #0000ff">null</span><span style="color: #000000"> ; ) { Entry</span><K,V> e =<span style="color: #000000"> old; old </span>=<span style="color: #000000"> old.next; </span><span style="color: #0000ff">if</span><span style="color: #000000"> (rehash) { e.hash </span>=<span style="color: #000000"> hash(e.key); } </span><span style="color: #0000ff">int</span> index = (e.hash & 0x7FFFFFFF) %<span style="color: #000000"> newCapacity; e.next </span>=<span style="color: #000000"> newMap[index]; newMap[index] </span>=<span style="color: #000000"> e; } } }</span></pre>
在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一復制到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經變了。這里對閥值啰嗦一下:比如初始值11、加載因子默認0.75,那么這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,一次類推。
2、其實這里是我的一個疑問,在計算索引位置index時,HashTable進行了一個與運算過程(hash & 0x7FFFFFFF),為什么需要做一步操作,這么做有什么好處?如果哪位知道,望指導,LZ不勝感激!!下面是計算key的hash值,這里hashSeed發揮了作用。
private int hash(Object k) { return hashSeed ^ k.hashCode(); }
相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table數組中的索引位置,然后迭代鏈表,匹配直到找到相對應key的value,若沒有找到返回null。
public synchronized V get(Object key) { 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與HashMap的區別
HashTable和HashMap存在很多的相同點,但是他們還是有幾個比較重要的不同點。
第一:我們從他們的定義就可以看出他們的不同,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什么?它是任何可將鍵映射到相應值的類的抽象父類,而AbstractMap是基於Map接口的骨干實現,它以最大限度地減少實現此接口所需的工作。
第二:HashMap可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:
當HashMap遇到為null的key時,它會調用putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是對象都可以。
if (key == null) return putForNullKey(value);
而當HashTable遇到null時,他會直接拋出NullPointerException異常信息。
if (value == null) { throw new NullPointerException(); }
第三:Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建議如果是涉及到多線程同步時采用HashTable,沒有涉及就采用HashMap,但是在Collections類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的Map對象,並把它作為一個封裝的對象來返回,所以通過Collections類的synchronizedMap方法是可以我們你同步訪問潛在的HashMap。這樣君該如何選擇呢???