上一篇咱們認識了什麼是Map、Hash,瞭解了Hash處理哈希衝突的幾種經常使用方法(拉鍊法、開放定址法),以及分析了JDK1.8版本的HashMap源碼,對Java集合框架有了初步的認識,咱們本篇繼續分析JDK1.8版本的Hashtable源碼,最後比較HashMap和Hashtable的區別。算法
Hashtable數組
注意是Hashtable不是HashTable(t爲小寫),這不是違背了駝峯定理了嘛?這還得從Hashtable的出生提及,Hashtable是在Java1.0的時候建立的,而集合的統一規範命名是在後來的Java2開始約定的,而當時又發佈了新的集合代替它,因此這個命名也一直使用到如今,因此Hashtable是一個過期的集合了,不推崇你們使用這個類,雖然說Hashtable是過期的了,咱們仍是有必要分析一下它,以便對Java集合框架有一個總體的認知。安全
首先Hashtable採用拉鍊法處理哈希衝突,是線程安全的,鍵值不容許爲null,而後Hashtable繼承自Dictionary,實現Map接口,Hashtable有幾個重要的成員變量table、count、threshold、loadFactor多線程
table:是一個Entry[]數據類型,而Entry實際是一個單鏈表框架
count:Hashtable的大小,即Hashtable中保存的鍵值對數量函數
threshold:Hashtable的閾值,用於判斷是否須要調整Hashtable的容量,threshold = 容量負載因子,threshold=11*0.75 取整即8this
loadFactor:用來實現快速失敗機制的spa
構造函數線程
Hashtable有4個構造函數3d
//無參構造函數 默認Hashtable容量是11,默認負載因子是0.75 public Hashtable() { this(11, 0.75f); } //指定Hashtable容量,默認負載因子是0.75 public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } //指定Hashtable的容量和負載因子 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; //new一個指定容量的Hashtable table = new Entry<?,?>[initialCapacity]; //閾值threshold=容量*負載因子 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } //包含指定Map的構造函數 public Hashtable(Map<? extends K, ? extends V> t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }
這裏的Hashtable容量和HashMap的容量就有區別,Hashtable並不要求容量是2的冪次方,而HashMap要求容量是2的冪次方。負載因子則默認都是0.75。
put方法
put方法是同步的,即線程安全的,這點和HashMap不同,還有具體的put操做和HashMap也存在很大的差異,Hashtable插入的時候是插入到鏈表頭部,而HashMap是插入到鏈表尾部。
//synchronized同步鎖,因此Hashtable是線程安全的 public synchronized V put(K key, V value) {
// Make sure the value is not null
//若是值value爲空,則拋出異常 至於爲何官方不容許爲空,下面給出分析
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//直接取key的hashCode()做爲哈希地址,這與HashMap的取hashCode()以後再進行hash()的結果做爲哈希地址 不同
int hash = key.hashCode();
//數組下標=(哈希地址 & 0x7FFFFFFF) % Hashtable容量,這與HashMap的數組下標=哈希地址 & (HashMap容量-1)計算數組下標方式不同,前者是取模運算,後者是位於運算,這也就是爲何HashMap的容量要是2的冪次方的緣由,效率上後者的效率更高。
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍歷Entry鏈表,若是鏈表中存在key、哈希地址相同的節點,則將值更新,返回舊值
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//若是爲新的節點,則調用addEntry()方法添加新的節點
addEntry(hash, key, value, index);
//插入成功返回null
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//若是當前鍵值對數量>=閾值,則執行rehash()方法擴容Hashtable的容量
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
//獲取key的hashCode();
hash = key.hashCode();
//從新計算下標,由於Hashtable已經擴容了。
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//獲取當前Entry鏈表的引用 復賦值給e
Entry<K,V> e = (Entry<K,V>) tab[index];
//建立新的Entry鏈表的 將新的節點插入到Entry鏈表的頭部,再指向以前的Entry,即在鏈表頭部插入節點,這個和HashMap在尾部插入不同。
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
hashCode()爲何要& 0x7FFFFFFF呢?由於某些對象的hashCode()多是負值,& 0x7FFFFFFF保證了進行%運算時候獲得的下標是個正數
get方法
get方法也是同步的,和HashMap不同,即線程安全,具體的get操做和HashMap也有區別。
//同步
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//和put方法同樣 都是直接獲取key的hashCode()做爲哈希地址
int hash = key.hashCode();
//和put方法同樣 經過(哈希地址 & 0x7FFFFFFF)與Hashtable容量作%運算 計算出下標
int index = (hash & 0x7FFFFFFF) % tab.length;
//遍歷Entry鏈表,若是鏈表中存在key、哈希地址同樣的節點,則找到 返回該節點的值,否者返回null
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
remove方法
//同步
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//遍歷Entry鏈表,e爲當前節點,prev爲上一個節點
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
//找到key、哈希地址同樣的節點
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
//若是上一個節點不爲空(即不是當前節點頭結點),將上一個節點的next指向當前節點的next,即將當前節點移除鏈表
if (prev != null) {
prev.next = e.next;
} else { //若是上一個節點爲空,即當前節點爲頭結點,將table數組保存的鏈表頭結點地址改爲當前節點的下一個節點
tab[index] = e.next;
}
//Hashtable的鍵值對數量-1
count--;
//獲取被刪除節點的值 而且返回
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
rehash方法
Hashtable的rehash方法和HashMap的resize方法同樣,是用來擴容哈希表的,可是擴容的實現又有區別。
protected void rehash() {
//獲取舊的Hashtable的容量
int oldCapacity = table.length;
//獲取舊的Hashtable引用,爲舊哈希表
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新的Hashtable容量=舊的Hashtable容量 2 + 1,這裏和HashMap的擴容不同,HashMap是新的Hashtable容量=舊的Hashtable容量 2。
int newCapacity = (oldCapacity << 1) + 1;
//若是新的Hashtable容量大於容許的最大容量值(Integer的最大值 - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//若是舊的容量等於容許的最大容量值則返回
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
//新的容量等於容許的最大容量值
newCapacity = MAX_ARRAY_SIZE;
}
//new一個新的Hashtable 容量爲新的容量
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//計算新的閾值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//擴容後遷移Hashtable的Entry鏈表到正確的下標上
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
接下來咱們執行如下代碼,驗證如下數據遷移過程
Hashtable hashtable = new Hashtable();
for (int i = 1; i <= 24; i ++) {
hashtable.put(String.valueOf(i), i);
}
for (int i = 25; i <= 80; i ++) {
hashtable.put(String.valueOf(i), i);
}
new一個Hashtable,默認容量是11,負載因子是0.75
執行第一個for循環後,20保存在下標爲0的Entry中,即(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 11 = 0
執行第二個for循環後,變成了20保存在下標爲70的Entry中,由於Hashtable擴容了4次,分別是從容量爲默認的11->23->47->95->191,而後此時容量是191,因此(hash &0x7FFFFFFF) % 容量 -> (1598 &0x7FFFFFFF) % 191 = 70
HashMap和Hashtable區別
到這裏咱們分析了HashMap和Hashtable的原理,如今比較如下他們的區別。
不一樣點
繼承的類不同:HashMap繼承的AbstractMap抽象類,Hashtable繼承的Dictionay抽象類
應對多線程處理方式不同:HashMap是非線程安全的,Hashtable是線程安全的,因此Hashtable效率比較低
定位算法不同:HashMap經過key的hashCode()進行hash()獲得哈希地址,數組下標=哈希地址 & (容量 - 1),採用的是與運算,因此容量須要是2的冪次方結果才和取模運算結果同樣。而Hashtable則是:數組下標=(key的hashCode() & 0x7FFFFFFF ) % 容量,採用的取模運算,因此容量沒要求
鍵值對規則不同:HashMap容許鍵值爲null,而Hashtable不容許鍵值爲null
哈希表擴容算法不同:HashMap的容量擴容按照原來的容量2,而Hashtable的容量擴容按照原來的容量2+1
容量(capacity)默認值不同:HashMap的容量默認值爲16,而Hashtable的默認值是11
put方法實現不同:HashMap是將節點插入到鏈表的尾部,而Hashtable是將節點插入到鏈表的頭部
底層結構不同:HashMap採用了數組+鏈表+紅黑樹,而Hashtable採用數組+鏈表
爲何HashMap容許null鍵值呢,而Hashtable不容許null鍵值呢?這裏還得先介紹一下什麼是null,咱們知道Java語言中有兩種類型,一種是基本類型還有一種是引用類型,其實還有一種特殊的類型就是null類型,它不表明一個對象(Object)也不是一個對象(Object),而後在HashMap和Hashtable對鍵的操做中使用到了Object類中的equals方法,因此若是在Hashtable中置鍵值爲null的話就可想而知會報錯了,可是爲何HashMap能夠呢?由於HashMap採用了特殊的方式,將null轉爲了對象(Object),具體怎麼轉的,這裏就不深究了。
相同點
實現相同的接口:HashMap和Hashtable均實現了Map接口
負載因子(loadFactor)默認值同樣:HashMap和Hashtable的負載因子默認都是0.75
採用相同的方法處理哈希衝突:都是採用鏈地址法即拉鍊法處理哈希衝突
相同哈希地址可能分配到不一樣的鏈表,同一個鏈表內節點的哈希地址不必定相同:由於HashMap和Hashtable都會擴容,擴容後容量變化了,相同的哈希地址取到的數組下標也就不同。