前言
上一篇我們認識了什麼是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 取整即8
- loadFactor:用來實現快速失敗機制的
建構函式
Hashtable
有4個建構函式
//無參建構函式 預設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都會擴容,擴容後容量變化了,相同的雜湊地址取到的陣列下標也就不一樣。
參考
HashMap、HashTable、ConcurrentHashMap的原理與區別
Java集合之Hashtable原始碼解析
原文地址:https://ddnd.cn/2019/03/08/jdk1-8-hashtable/