在進行Hashtable原始碼解析之前,我先扔出Hashtable與HashMap有哪些區別?
1.關於null,HashMap允許key和value都可以為null,而Hashtable則不接受key為null或value為null的鍵值對。
2.關於執行緒安全,HashMap是執行緒不安全的,Hashtable是執行緒安全的,因為Hashtable的許多操作函式都用synchronized修飾。
3.Hashtable與HashMap實現的介面一致,但Hashtable繼承Dictionary,而HashMap繼承自AbstractMap,即父類不同。
4.預設初始容量不同,擴容大小不同。HashMap的hash陣列的預設大小是16,而且一定是2 的指數,增加方式old2;Hashtable中hash陣列預設大小是11,增加的方式是old2+1。
之前簡要分析過HashMap的程式碼,關於HashMap可點選:Java集合之HashMap原始碼解析 .下面我們開始進行Hashtable的分析,先看程式碼清單1:
public class HashtableTest {
public static void main(String [] args){
Hashtable<String, String> table=new Hashtable<>();
Hashtable<String, String> table1=new Hashtable<>(16);
Hashtable<String, String> table2=new Hashtable<>(16, 0.75f);
HashMap<String,String> map=new HashMap<>();
Hashtable<String,String> table3=new Hashtable<>(map);
table.put("T1", "1");
table.put("T2", "2");
table.put(null, "3");
System.out.println();
System.out.println(table.toString());
}
}複製程式碼
我們看到在建立物件上,和HashMap的使用方式相似,我們繼續看其內部的建構函式是怎麼樣的,如程式碼清單2:
private transient Entry<?,?>[] table;//陣列
private transient int count;//鍵值對的數量
private int threshold;//閥值
private float loadFactor;//載入因子
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;
this.loadFactor = loadFactor;
table = new Entry[initialCapacity];//這裡是與HashMap的區別之一,HashMap中table
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
initHashSeedAsNeeded(initialCapacity);
}
public Hashtable(int initialCapacity) {//指定初始陣列長度
this(initialCapacity, 0.75f);
}
public Hashtable() {//從這裡可以看出容量的預設值為16,載入因子為0.75f.
this(11, 0.75f);
}
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}複製程式碼
Hashtable的建構函式主要對table陣列進行了初始化,這裡沒有什麼要拿出來詳細講的,我們還是看下put的流程,程式碼清單3:
public synchronized V put(K key, V value) {//這裡方法修飾符為synchronized,所以是執行緒安全的。
if (value == null) {
throw new NullPointerException();//value如果為Null,丟擲異常
}
Entry tab[] = 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)) {
V old = e.value;
e.value = value;
return old;
}
}
modCount++;//修改次數。
if (count >= threshold) {//鍵值對的總數大於其閥值
rehash();//在rehash裡進行擴容處理
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
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了。
}
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
int newCapacity = (oldCapacity << 1) + 1;//擴容,如果預設值是11,則擴容之後,陣列的長度為23
if (newCapacity - MAX_ARRAY_SIZE > 0) {//這裡的最大值和HashMap裡的最大值不同,這裡Max_ARRAY_SIZE的是因為有些虛擬機器實現會限制陣列的最大長度。
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 rehash = initHashSeedAsNeeded(newCapacity);
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;
}
}
}複製程式碼
通過程式碼清單2和3,我們可以看到Hashtable的資料結構和HashMap一樣是一個Entry的陣列,陣列元素是一個單連結串列,這裡展示出了一個很明顯的區別,即hash值沒有擾動處理。那怎麼解決衝突呢?Hashtable的索引求值公式其實是:
((hashSeed^k.hashCode())&0x7FFFFFFF)%newCapacity——> hash&0x7FFFFFFF%newCapacity。hash&0x7FFFFFF是為了保證正數,因為hashCode的值有可能為負值,這樣說有可能會有同學說,取正數直接用Math.abs不就行了。。。但是你確定所有情況下,abs都能保證輸出是正數嗎?來來舉個例子給你看看:
哈哈,你還能說什麼。hash&0x7FFFFFFF是為了避免負值的出現,對newCapacity求餘是為了使index在陣列索引範圍之內。看到這估計就有人問了那麼HashMap中的hash&(tab.leng-1)怎麼解釋呢?如果不太明白,還請大家仔細看上篇文章,這裡簡單說一下,hash&(tab.length-1)其實是對(hash&0x7FFFFFFF)%newCapacity的程式碼級優化,簡而言之就是位運算的效能是優於求餘運算的。
細心的讀者可能會發現HashMap與Hashtable的最大值是不一樣的,上篇文章我們說過HashMap的長度是2的指數值,而HashMap的陣列的最大長度是:1<<30=1073741824,這個值是int範圍內2的指數值的最大值,不信可以列印1<<31=-2147483648。關於Hashtable的陣列的最大值原始碼註釋中有說明,是指虛擬機器實現對array的長度有限制,如果大家糾結於這個最大值,why the max value is Integer.MAX_SIZE-8,請參考下面兩個連結:
stackoverflow.com/questions/3…
stackoverflow.com/questions/3…
說完了Hash與最大值的梗,我們就要來看看執行緒安全,Hashtable中的主要方法都加了synchronized關鍵字來修飾(沒有被顏色覆蓋的),如下:
前面我們已經分析過存資料的過程,現在我們一起來看看取的過程。
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;
}複製程式碼
鑑於Hashtable是歷史遺留的類,現在很少有人使用它,即使我們在對執行緒安全有要求的場景中,也是通過使用ConcurrentHashMap來解決,而不是使用Hashtable 。這裡可以簡要的說一下原因:Hashtable使用synchronized來實現執行緒安全,效率不高,而ConcurrentHashMap採用鎖分段技術來實現執行緒安全,大大提高了效率。在多執行緒環境中,當A執行緒訪問Hashtable的put方法時,其他執行緒是不能訪問諸如get,clear這些方法的,但是在ConcurrentHashMap中只要保證A執行緒與B執行緒不是持有一個段鎖,是可以A執行緒訪問put時其他執行緒同時訪問get操作。
最後我們說一下Hashtable的初始容量為什麼是11?Hashtable的擴容方式是:old*2+1,初始容量11,第一次擴容為23,第二次擴容為47,可以看到Hashtable的容量肯定是奇數,有一些更是為質數。到這裡就涉及到了雜湊演算法相關的知識了,這裡就不展開說雜湊演算法相關的內容了。Hashtable之所以初始容量為11(質數)和擴容方式保證為奇數,是為了雜湊得更均勻,也就是減少碰撞發生的機率。
轉載請註明出處:blog.csdn.net/android_jia…