一、概述
Hashtable類繼承於Dictionary抽象類,jdk註釋中說明了Dictionary類已經過時,新的實現類應該去實現Map接口,而不是繼承Dictionary類。但是面試的時候還是常常會問到Hashtable與HashMap的區別,所以我們還是來看一下Hashtable類的源碼,以及現在的實際應用場景中用什麼類來代替它。
二、源碼分析
(1) 類的聲明
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
與HashMap類相比,實現的接口完全一致,只是集成的父類不同:Hashtable繼承了Dictionary類;HashMap則是繼承自AbstractMap類。
(2) 成員變量
//存放Entry元素的數組
private transient Entry<?,?>[] table;
//實際元素的數量
private transient int count;
//擴容的臨界容量,threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
private int threshold;
//負載因子
private float loadFactor;
//修改標記,用於fail-fast機制
private transient int modCount = 0;
//指定的序列化標識ID
private static final long serialVersionUID = 1421746759512286392L;
//數組最大容量,留8個字節存儲對象頭,具體可在jvm的學習中瞭解
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
transient修飾符修飾的成員變量不會被序列化。
(3) 構造方法
//默認無參構造函數,設置默認的容量爲11,負載因子爲0.75f
public Hashtable() {
this(11, 0.75f);
}
//指定容量的構造函數
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
//指定容量和負載因子的構造函數
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);
//如果指定容量爲0,那麼返回的容量爲1
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
//傳入指定的Map返回爲Hashtable
public Hashtable(Map<? extends K, ? extends V> t) {
//設置容量Math.max(2*t.size(), 11),取2倍t的元素數量和11比較,取大的值
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
不同於HashMap的是,Hashtable在成員變量中並沒有設置默認容量,而是在構造函數中設置的,並且默認容量爲11;HashMap是成員變量中就設置了初始容量爲16。
(4) Entry<K,V>源碼如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
final 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;
}
//這裏與HashMap的Node不同,HashMap的Node沒有clone()方法
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
//設置value時,如果value爲null,則直接拋出異常
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;
//必須是key和value都相等才返回true
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
//hashCode()與HashMap也不同,HashMap是將key和value的hash值進行異或運算。
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
(5) addEntry()方法
//添加一個Entry元素到指定位置index
private void addEntry(int hash, K key, V value, int index) {
//修改標記+1
modCount++;
Entry<?,?> tab[] = table;
//如果元素數量超過了限制的數量,就調用rehash()方法進行擴容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
//用k的hash值與Integer.MAX_VALUE-1進行&與運算後的結果對table的容量取模獲得新下標
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
//創建新元素,並獲取舊元素e的引用
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
//將index位置設置爲新元素,且新元素的下一個元素指定爲e,也就是說每次添加元素都是添加在鏈表頭
tab[index] = new Entry<>(hash, key, value, e);
//元素總數+1
count++;
}
(6) rehash()方法
//擴容方法
protected void rehash() {
//記錄原始的容量爲oldCapacity
int oldCapacity = table.length;
//記錄下原始的容器爲oldMap
Entry<?,?>[] oldMap = table;
//計算新容量newCapacity的值爲2倍oldCapacity的值+1,也就是newCapacity = 2oldCapacity + 1
int newCapacity = (oldCapacity << 1) + 1;
//如果新容量大於數組最大運行容量MAX_ARRAY_SIZE,也就是Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//判斷原始容量oldCapacity是否已經等於了MAX_ARRAY_SIZE,如果是則直接return
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
//如果原始容量oldCapacity還未達到MAX_ARRAY_SIZE,則將此次新容量newCapacity設置爲MAX_ARRAY_SIZE
newCapacity = MAX_ARRAY_SIZE;
}
//用新容量初始化一個Entry數組
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//修改標記+1
modCount++;
//計算新的擴容臨界值threshold,取新容量newCapacity和負載因子loadFactor的乘積與MAX_ARRAY_SIZE + 1中較小的值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//將新Map設置爲容器
table = newMap;
//循環將舊容器oldMap中的元素添加到新容器中
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;
//重新計算hash值
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//把newMap原來index下的元素設置爲e的下一個元素,並將元素e放在index位置上
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
HashTable的擴容機制如下:例如默認初始容量是11,加載因子爲0.75,那麼擴容閥值就是8,當數組長度達到8的時候,HashTable就會進行一第次擴容,擴容後的容量就是 8 * 2 + 1 = 17 ( int newCapacity = (oldCapacity << 1) + 1) ,此時的擴容閥值就是 17 * 0.75 = 13 ,當下次達到13的時候,就會在重複擴容一次。其實,這個擴容消耗還是蠻大的,因爲擴容後需要原來HashTable中的元素一一複製到新的HashTable中。
(7) put()方法
//添加一個元素
public synchronized V put(K key, V value) {
// 如果value爲null,直接拋出異常
if (value == null) {
throw new NullPointerException();
}
//獲取現有的容器tab[]
Entry<?,?> tab[] = table;
//計算桶位,如果key爲null,此處會拋異常
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//循環遍歷是否有相同key的元素存在,如果有就替換舊元素的value值,並且返回舊元素的alue值
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果遍歷完,不存在相同的key,則調用addEntry()方法添加元素
addEntry(hash, key, value, index);
return null;
}
我們可以看到Hashtable類的put()方法加入了synchronized關鍵字修飾,以確保此方法線程安全。
(8) get()方法
//獲取指定key的value值,線程安全
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//計算hash
int hash = key.hashCode();
//獲取下標
int index = (hash & 0x7FFFFFFF) % tab.length;
//遍歷鏈表,找到元素返回
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
//沒有返回 null
return null;
}
(9) clear()方法
//將元素全部置爲null,也是線程安全的
public synchronized void clear() {
Entry<?,?> tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; )
tab[index] = null;
count = 0;
}
(10) containsKey()方法
//判斷是否包含key,線程安全
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//找到“key對應的Entry(鏈表)”,然後在鏈表中找出“哈希值”和“鍵值”與key都相等的元素
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}
沒啥好說的,就是拿key直接轉化爲座標index,從index往後找,查找是否存在此key的元素,判斷的依據是hash值和key值都要相同。
(11) containsValue()方法
public boolean containsValue(Object value) {
return contains(value);
}
public synchronized boolean contains(Object value) {
//如果value爲null直接拋異常
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//循環遍歷容器
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
三、總結
Hashtable類還是要與HashMap來比較着分析,主要有一下幾點不同:
* 繼承的父類不同:Hashtable類繼承自Dictionary這一過時的類;HashMap類繼承自AbstractMap類。
* 數據結構不同:Hashtable始終是"數組+鏈表"的形式;HashMap在jdk1.8後是有"數組+鏈表"和"數組+紅黑樹"的形式的。
* 無參初始容量不同:Hashtable無參初始容量爲11;HashMap的無參初始容量爲16
* hash值計算方式不同:HashTable計算哈希的方式是直接取key本身的hash;而HashMap計算hash的方式爲"(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) ",即自身哈希和哈希無符號右移16位做與運算。
* 獲得索引key的方式不同:Hashtable是“index = (hash & 0x7FFFFFFF) % tab.length”,採用的是取模運算;而HashMap在jdk1.8已經採用“(length - 1) & hash”,把hash值和容量進行“與”操作,這得益於HashMap的容量始終爲2的次冪,這樣計算效率大大提升。
* 擴容機制不同:一般情況下,Hashtable每次擴容是從n到2n+1;HashMap每次擴容從n變爲2n,並且由於HashMap在指定容量進行初始化以及每次擴容時都會調用 inflateTable()方法來保證自己的容器容量始終是2的次冪。
* key和value限制不同:Hashtable不允許key爲null,也不允許value爲null,源碼中我們得知,每次都會判斷value是否爲null,如果是就直接拋出異常,而key則是在調用key.hashCode()時,如果key爲null也會拋出異常;HashMap中的源碼在判斷key爲null後,會設置key的hash值爲0,也就是放在桶的第一個位置,代碼中也不會value是否爲null做限制,那麼結論就是HashMap允許一個key爲null的元素(再有就覆蓋原來的value),允許多個value爲null的元素。
* 線程安全問題:Hashtable中涉及容器變化以及訪問的方法,都採用了synchronized關鍵字修飾,以保證線程安全,相對的效率低一些;HashMap無synchronized修飾,線程不安全,相比Hashtable效率高一些。
值得注意的是Hashtable類似乎也要被淘汰了,jdk1.8的Hashtable類的註釋中有寫: 如果你不需要線程同步,建議使用HashMap來代替HashTable,如果你的你是需要線程同步的話使用ConcurrentHashMap來替代Hashtable 。
更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!