概述
基於J11,該類已經淘汰,如果使用執行緒安全的則用 ConcurrentHashMap
,用執行緒不安全的則使用 HashMap
。僅與HashMap進行比較
結構以及依賴關係
HashTable 的結構如下圖
當遇到有同樣 Hash 值的情況,會通過連結串列來解決衝突問題(連結法,通過連結串列解決衝突問題)。
連結法會隨著衝突的增多導致查詢時間越來越慢。會出現一種惡劣的情況,當雜湊演算法特別差時;元素總數n和某個槽位數 m 中的 k 相等,如下圖所示
在這種情況下,查詢的時間為 $O(1+a)$ 其中 $O(1)$ 為hash
通過下圖可以得知 Hashtable 與其他類的關係
實際上,Hashtable中的每個元素都是一個 Map.Entry<k,v>
,Entry
是 Map
的集合形式 用來遍歷Map
。Hashtable實現了該介面,Hashtable就是一個集合,不過儲存的是一個一個連結串列。
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
public K getKey(){...}
public V getValue(){...}
public V setValue(V value){...}
}
Hashtable有幾個關鍵的欄位需要注意:
private int threshold; // 可容納的極限長度,容量*負載因子
private float loadFactor; // 負載因子 該值預設為0.75
如果把Hashtbale比做桶,負載因子就表明一桶水能裝半桶還是裝滿桶還是裝四分之一桶。
負載因子越大,能裝的水就越多。負載因子總和臨界值配合,臨界值用來表示什麼時候擴容,也就是水裝不下了得換一個大一點的桶裝水。Hashtable每一次擴容都會擴大到原來的兩倍大。
負載因子是對時間和空間的平衡,當負載因子增大空間會比較充足就不需要總是擴容,空間用的較多;如果負載因子小需要不斷擴容,但是空間用的少。
通過一個put方法來了解
下圖簡述了put的流程
計算位置
Hashtable中計算位置特別簡單,就是簡單的除法
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
插入元素
首先,Hashtable需要知道當前put操作是更新舊值還是插入新值。如果更新舊值就返回舊值並更新它
下面就是一個不斷查詢連結串列的過程
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
如果是插入新值則建立一個 Entry
並插入,這是在容量沒有超過臨界值的情況:
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
modCount++;
當然,如果容量超過臨界值則需要擴容
擴容
if (count >= threshold) {
// 擴容,並重新計算每個元素的hash值
rehash();
// 擴容之後插入新值
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
擴容的關鍵是 rehash()
這個方法。該方法也很簡單,只有以下幾個步驟:
- 計算新的臨界值
- 新臨界值超過最大能接受的容量則不再擴充
- 建立一個新table(新的大桶)
- 逐個計算hash值並重新裝填table
執行緒安全性
Hashtable是執行緒安全的,主要是通過為每個方法加入一個同步鎖來解決,如put方法
public synchronized V put(K key, V value) {...}
但是這樣效能還是比較低的,同時不能保證組合方法的執行緒安全性。
例如 get
和 remove
public V getAndRemove(Object o){
V v = get(o);
remove(o);
return v;
}
這樣是不能保證執行緒安全的