【原始碼】Hashtable原始碼剖析
阿新 • • 發佈:2019-02-08
注:以下原始碼基於jdk1.7.0_11
上一篇分析了HashMap的原始碼,相信大家對HashMap都有了更深入的理解。本文將介紹Map集合的另一個常用類,Hashtable。 Hashtable出來的比HashMap早,HashMap 1.2才有,而Hashtable在1.0就已經出現了。HashMap和Hashtable實現原理基本一樣,都是通過雜湊表實現。而且兩者處理衝突的方式也一樣,都是通過連結串列法。下面我們就詳細介紹下這個類。 首先看類宣告:
Hashtable並沒有去繼承AbstractMap,而是選擇繼承了Dictionary類,Dictionary是個被廢棄的抽象類,文件已經說得很清楚了:
這個類的方法如下(全是抽象方法):
下面是獲取其鍵集(keySet)和鍵值集(entrySet)的方法:
這個KeySet和EntrySet是Hashtable的兩個內部類:
總結: 1.Hashtable是個執行緒安全的類(HashMap執行緒安全); 2.Hasbtable並不允許值和鍵為空(null),若為空,會拋空指標(HashMap可以); 3.Hashtable不允許鍵重複,若鍵重複,則新插入的值會覆蓋舊值(同HashMap); 4.Hashtable同樣是通過連結串列法解決衝突; 5.Hashtable根據hashcode計算索引時將hashcode值先與上0x7FFFFFFF,這是為了保證hash值始終為正數; 6.Hashtable的容量為任意正數(最小為1),而HashMap的容量始終為2的n次方。Hashtable預設容量為 11,HashMap預設容量為 16; 7.Hashtable每次擴容,新容量為舊容量的2倍加2,而HashMap為舊容量的2倍; 8.Hashtable和HashMap預設負載因子都為0.75;
上一篇分析了HashMap的原始碼,相信大家對HashMap都有了更深入的理解。本文將介紹Map集合的另一個常用類,Hashtable。 Hashtable出來的比HashMap早,HashMap 1.2才有,而Hashtable在1.0就已經出現了。HashMap和Hashtable實現原理基本一樣,都是通過雜湊表實現。而且兩者處理衝突的方式也一樣,都是通過連結串列法。下面我們就詳細介紹下這個類。 首先看類宣告:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
Hashtable並沒有去繼承AbstractMap,而是選擇繼承了Dictionary類,Dictionary是個被廢棄的抽象類,文件已經說得很清楚了:
NOTE: This class is obsolete. New implementations should
* implement the Map interface, rather than extending this class.
這個類的方法如下(全是抽象方法):
public abstract class Dictionary<K,V> { public Dictionary() { } abstract public int size(); abstract public boolean isEmpty(); abstract public Enumeration<K> keys(); abstract public Enumeration<V> elements(); abstract public V get(Object key); abstract public V put(K key, V value); abstract public V remove(Object key); }
沒啥好說的,下面直接看Hashtable原始碼,首先依然是成員變數:
成員變數跟HashMap基本類似,但是HashMap更加規範,HashMap內部還定義了一些常量,比如預設的負載因子,預設的容量,最大容量等等。 接下來是構造器:private transient Entry<K,V>[] table;//儲存鍵值對物件的桶陣列 /** * The total number of entries in the hash table. *鍵值對總數 */ private transient int count; /** * The table is rehashed when its size exceeds this threshold. (The * value of this field is (int)(capacity * loadFactor).) *容量的閾值,超過此容量將會導致擴容。值為容量*負載因子 */ private int threshold; /** * The load factor for the hashtable. *負載因子 */ private float loadFactor; /** * hashtable被改變的次數,用於快速失敗機制 */ private transient int modCount = 0;
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;//初始容量最小值為1
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];//建立桶陣列
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化容量閾值
useAltHashing = sun.misc.VM.isBooted() &&
(initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
}
/**
* Constructs a new, empty hashtable with the specified initial capacity
* and default load factor (0.75).
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);//預設負載因子為0.75
}
public Hashtable() {
this(11, 0.75f);//預設容量為11,負載因子為0.75
}
/**
* Constructs a new hashtable with the same mappings as the given
* Map. The hashtable is created with an initial capacity sufficient to
* hold the mappings in the given Map and a default load factor (0.75).
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
需注意的點:
1.Hashtable的預設容量為11,預設負載因子為0.75.(HashMap預設容量為16,預設負載因子也是0.75)
2.Hashtable的容量可以為任意整數,最小值為1,而HashMap的容量始終為2的n次方。
3.為避免擴容帶來的效能問題,建議指定合理容量。
另外我們看到,Hashtable的編碼相比較HashMap不是很規範,構造器中出現了硬編碼,而HashMap中定義了常量。
跟HashMap一樣,Hashtable內部也有一個靜態類叫Entry,其實是個鍵值對物件,儲存了鍵和值的引用。也可以理解為一個單鏈表的結點,因為其持有下一個Entry物件的引用:
private static class Entry<K,V> implements Map.Entry<K,V> {//鍵值對物件
int hash;//雜湊值
final K key;//鍵
V value;//值
Entry<K,V> next;//指向下一個
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
protected Object clone() {//直接通過new的方式克隆
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {//可設定值
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry)o;
return key.equals(e.getKey()) && value.equals(e.getValue());
}
public int hashCode() {
return hash ^ value.hashCode();
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
再次強調:HashMap和Hashtable儲存的是鍵值對物件,而不是單獨的鍵或值。
明確了儲存方式後,再看put和get方法:
public synchronized V put(K key, V value) {//向雜湊表中新增鍵值對
// Make sure the value is not null
if (value == null) {//確保值不能為空
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = hash(key);//根據鍵生成hash值---->若key為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)) {//若鍵相同,則新值覆蓋舊值
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//當前容量超過閾值。需要擴容
// Rehash the table if the threshold is exceeded
rehash();//重新構建桶陣列,並對陣列中所有鍵值對重雜湊,耗時!
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;//這裡是取摸運算
}
// Creates the new entry.
Entry<K,V> e = tab[index];
//將新結點插到連結串列首部
tab[index] = new Entry<>(hash, key, value, e);//生成一個新結點
count++;
return null;
}
需注意的點:
1.Hasbtable並不允許值和鍵為空(null),若為空,會拋空指標.大家可能奇怪,put方法在開始處僅對value進行判斷,並未對key判斷,這裡我認為是設計者的疏忽。當然,這並不影響使用,因為當呼叫hash方法時,若key為空,依然會丟擲空指標異常:
private int hash(Object k) {
if (useAltHashing) {
if (k.getClass() == String.class) {
return sun.misc.Hashing.stringHash32((String) k);
} else {
int h = hashSeed ^ k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
} else {
return k.hashCode();//此處可能拋空指標異常
}
}
2.HashMap計算索引的方式是h&(length-1),而Hashtable用的是模運算,效率上是低於HashMap的。
3.另外Hashtable計算索引時將hash值先與上0x7FFFFFFF,這是為了保證hash值始終為正數。
4.特別需要注意的是這個方法包括下面要講的若干方法都加了synchronized關鍵字,也就意味著這個Hashtable是個執行緒安全的類,這也是它和HashMap最大的不同點.
下面我們看下擴容方法rehash:
protected void rehash() {
int oldCapacity = table.length;//記錄舊容量
Entry<K,V>[] oldMap = table;//記錄舊的桶陣列
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;//新容量為老容量的2倍加1
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)//容量不得超過約定的最大值
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<K,V>[] newMap = new Entry[newCapacity];//建立新的陣列
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
boolean currentAltHashing = useAltHashing;
useAltHashing = sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = currentAltHashing ^ useAltHashing;
table = newMap;
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;
}
}
}
Hashtable每次擴容,容量都為原來的2倍加2,而HashMap為原來的2倍。
接下來分析get方法:
public synchronized V get(Object key) {//根據鍵取出對應索引
Entry tab[] = table;
int hash = hash(key);//先根據key計算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//再根據hash值找到索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍歷entry鏈
if ((e.hash == hash) && e.key.equals(key)) {//若找到該鍵
return e.value;//返回對應的值
}
}
return null;//否則返回null
}
當然,如果你傳的引數為null,是會拋空指標的。
至此,最重要的部分已經講完,下面再看一些常用的方法:
public synchronized V remove(Object key) {//刪除指定鍵值對
Entry tab[] = table;
int hash = hash(key);//計算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//計算索引
for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {//遍歷entry鏈
if ((e.hash == hash) && e.key.equals(key)) {//找到指定鍵
modCount++;
if (prev != null) {//修改相關指標
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
public synchronized void clear() {//清空桶陣列
Entry tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;//直接置空
count = 0;
}
下面是獲取其鍵集(keySet)和鍵值集(entrySet)的方法:
public Set<K> keySet() {
if (keySet == null)//通過Collections的包裝,返回的是執行緒安全的鍵集
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
public Set<Map.Entry<K,V>> entrySet() {
if (entrySet==null)//通過Collections的包裝,返回的是執行緒安全的鍵值集
entrySet = Collections.synchronizedSet(new EntrySet(), this);
return entrySet;
}
這個KeySet和EntrySet是Hashtable的兩個內部類:
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return getIterator(KEYS);
}
public int size() {
return count;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return Hashtable.this.remove(o) != null;
}
public void clear() {
Hashtable.this.clear();
}
}
總結: 1.Hashtable是個執行緒安全的類(HashMap執行緒安全); 2.Hasbtable並不允許值和鍵為空(null),若為空,會拋空指標(HashMap可以); 3.Hashtable不允許鍵重複,若鍵重複,則新插入的值會覆蓋舊值(同HashMap); 4.Hashtable同樣是通過連結串列法解決衝突; 5.Hashtable根據hashcode計算索引時將hashcode值先與上0x7FFFFFFF,這是為了保證hash值始終為正數; 6.Hashtable的容量為任意正數(最小為1),而HashMap的容量始終為2的n次方。Hashtable預設容量為 11,HashMap預設容量為 16; 7.Hashtable每次擴容,新容量為舊容量的2倍加2,而HashMap為舊容量的2倍; 8.Hashtable和HashMap預設負載因子都為0.75;