在上一篇博客 Java容器之HashMap源碼分析(媽媽再也不用擔心我不懂HashMap了) 從源碼層次分析了HashMap
容器的底層實現,在本篇博客將繼續從源碼層次分析Hashtable
的底層實現。
註明:以下源碼分析都是基於jdk 1.8.0_221
版本
Hashtable源碼分析目錄
一、Hashtable
容器概述(一圖以蔽之)
Java
中的Hashtable
容器也是一個用來存放key-value
(鍵值對)的容器,主要由table數組
和若干鏈表
組成。
Hashtable
容器類的聲明如下:
public class Hashtable<K,V> extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable
註明:既然Java
已經有了HashMap
容器,爲啥還有引入Hashtable
?應該有部分小夥伴對此有疑問。通過對比HashMap
、Hashtable
容器的功能,可以發現,兩者都是key-value
容器,不過HashMap
是線程不安全的容器,即不能多個線程同時訪問一個HashMap
容器;而Hashtable
容器是線程安全的,即Hashtable
容器支持多個線程同時訪問(通過synchronized
關鍵字,this
鎖實現)。
二、Hashtable
類的屬性
Hashtable
類中的主要屬性如下:
/**
* table數組(hash桶數組),用於存放key-value數據
*/
private transient Entry<?,?>[] table;
/**
* 容器中已經存放的key-value個數
*/
private transient int count;
/**
* 容器可存放key-value的閾值 = capacity * loadFactor
* capacity是容器的容量(table數組的長度,初始化時默認是11)
* loadFactor是負載因子,默認是0.75
*/
private int threshold;
/**
* 負載因子,默認是0.75
*/
private float loadFactor;
/**
* 用於記錄容器結構性調整(增刪Entry、rehash等)的次數
*/
private transient int modCount = 0;
/**
* 序列化版本ID
*/
private static final long serialVersionUID = 1421746759512286392L;
/**
* keySet用於存放容器中的所有key
* values用於存放容器中的所有value
* entrySet以set容器形式存放Entry,一般遍歷Hashtable會用到此屬性
*/
private transient volatile Set<K> keySet;
private transient volatile Set<Map.Entry<K,V>> entrySet;
private transient volatile Collection<V> values;
capacity
是容器的容量(table
數組的長度,初始化時默認是11),count
記錄容器中已經存放的key-value
個數,threshold
記錄容器可存放key-value
的閾值 = 容器容量
* 負載因子
。
三、Hashtable
類的構造器
Hashtable
類有四個構造器,分別如下:
/**
* @param initialCapacity 容器容量(table數組的長度)初始化參數
* @param loadFactor 負載因子
* @exception IllegalArgumentException initialCapacity、loadFactor參數非法異常
*/
public Hashtable(int initialCapacity, float loadFactor) {
// 檢查initialCapacity、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];
// 計算容器可存放的key-value數量閾值,容量 * 負載因子
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
/**
* 默認負載因子爲0.75的構造器
*
* @param initialCapacity 容器容量(table數組的長度)初始化參數
* @exception IllegalArgumentException initialCapacity參數非法異常
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* 容器容量默認爲11,負載因子默認爲0.75
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* 複製構造函數,用其它map容器初始化當前new的容器對象
*/
public Hashtable(Map<? extends K, ? extends V> t) {
// 先調用構造函數public Hashtable(int initialCapacity, float loadFactor)
// 一般Hashtable默認容量大小爲11,所以不能比它還小
this(Math.max(2*t.size(), 11), 0.75f);
// 然後將容器t中的所有entry複製到新建的容器對象中
putAll(t);
}
四、增加key-value
相關方法
1、addEntry
方法
addEntry
方法的作用是往容器中放入一個entry
,注意此方法沒有使用synchronized
關鍵字修飾,但是用的private
關鍵字修飾,也就是說不對外開發。只要是在本類調用addEntry
的其它方法是線程同步,那麼也不會產生線程不安全的狀況。
/**
* @parm hash 通過調用hashCode()得到的hash值
* @parm key 鍵值對中的key
* @parm value 鍵值對中的value
* @parm index hash對應的table數組的預期下標
*/
private void addEntry(int hash, K key, V value, int index) {
// 插入節點,屬於結構性調整
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// 如果容器當前存放的key-value數量達到了threshold閾值(容器容量 * 負載因子)
// 則需要進行rehash擴容操作
rehash();
// 由於進行rehash操作,則key對應的hash需要重新計算,hash對應的下標也要重新計算
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 根據key-value,新建一個entry,放入tab[index]中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
// 注意Entry的構造函數的第四個參數是next,也就是把tab[index] = new Entry
// 然後讓new Entry.next = e(未更新前tab[index]指向的內容)
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
2、put
方法
put
方法用了synchronized
、public
關鍵字修飾,對外開放,並且是線程安全。(synchronized
關鍵字修飾非靜態方法,用的是this鎖
)
/**
* @param key the hashtable key
* @param value the value
* @return 如果容器已經有key,更新value並且返回舊value,否則插入並且返回空
* @exception NullPointerException if the key or value is
* <code>null</code>
*/
public synchronized V put(K key, V value) {
// 確保value不能爲空
if (value == null) {
throw new NullPointerException();
}
// 計算key對應的hash以及hash對應在table數組中的下標
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// entry直線table[index],即hash對應的鏈表頭部
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 遍歷這個鏈表
for(; entry != null ; entry = entry.next) {
// 如果key已經存在,修改新的value,並且返回之前的value
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 否則調用addEntry方法插入,當前方法已經加了synchronized關鍵字,所以addEntry不會產生線程不安全的狀況
addEntry(hash, key, value, index);
return null;
}
3、putAll
方法
putAll
方法的作用是將其他map容器中的元素複製到當前容器中,使用了synchronized
關鍵字修飾,所以不會引發線程安全的狀況。
/**
* 作用是將其他map容器中的元素複製到當前容器中
*/
public synchronized void putAll(Map<? extends K, ? extends V> t) {
// 遍歷容器t將key-value一一複製到當前容器中
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
五、刪除key-value
相關方法
1、remove(key)
方法
remove(key)
方法的作用是通過key
刪除key-value
。
/**
* 通過key移除key-value
* @param key the key that needs to be removed
* @return 移除成功返回key對應的value,否則返空
* @throws NullPointerException if the key is <code>null</code>
*/
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
// 計算key對應的hash值,從而找到key在table數組中的下標
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 獲取hash值對應的鏈表頭部
Entry<K,V> e = (Entry<K,V>)tab[index];
// 遍歷此鏈表,pre指向的e前面的節點
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
// key匹配成功
// 移除節點也是結構性調整
modCount++;
if (prev != null) {
// 如果e前面(pre)不爲空,則直接把e移除鏈表即可
prev.next = e.next;
} else {
// 否則e就是鏈表的表頭tab[index],則需要更新tab[index]
tab[index] = e.next;
}
count--;
// 返回移除前key-value中的value
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
2、remove(key,value)
方法
remove(key,value)
方法是通過key-value刪除,只有兩個都匹配成功才進行刪除操作。此方法同樣使用了synchronized
關鍵字修飾,所以不會引發線程不安全的問題。
public synchronized boolean remove(Object key, Object value) {
Objects.requireNonNull(value);
Entry<?,?> tab[] = table;
// 計算key對應的hash值,從而找到key在table數組中的下標
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 獲取hash值對應的鏈表頭部
Entry<K,V> e = (Entry<K,V>)tab[index];
// 遍歷此鏈表,pre指向的e前面的節點
for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
// key匹配成功
// 移除節點也是結構性調整
modCount++;
if (prev != null) {
// 如果e前面(pre)不爲空,則直接把e移除鏈表即可
prev.next = e.next;
} else {
// 否則e就是鏈表的表頭tab[index],則需要更新tab[index]
tab[index] = e.next;
}
count--;
e.value = null;
// 刪除成功則返回true
return true;
}
}
return false;
}
六、查找key-value
相關方法
1、get(key)
方法
get(key)
方法的作用是通過key
查找value
,該方法同樣使用了synchronized
關鍵字修飾。
/**
* 通過`key`查找`value`
*/
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 計算key對應的hash值,從而找到key在table數組中的下標
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍歷tab[index]這個鏈表,查找key對應的value
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
2、containsKey(key)
方法
containsKey(key)
方法的作用是判斷容器中是否存在key
,該方法也使用了synchronized
關鍵字修飾。
/**
* 判斷容器中是否竄在key
*/
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
// 計算key對應的hash值,從而找到key在table數組中的下標
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍歷tab[index]鏈表,查找key
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}
3、containsValue(value)
方法
containsValue(value)
方法判斷容器中是否存在value
,該方法也使用了synchronized
關鍵字修飾。
/**
* 判斷容器中是否存在`value`
*/
public synchronized boolean contains(Object value) {
if (value == null) {
// 由於Hashtable的key、value都不能爲空,所以爲null的value不需要查找
throw new NullPointerException();
}
// 此時需要遍歷整個table數組
Entry<?,?> tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
// 遍歷tab[i] 每個鏈表
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
七、其它方法
1、hashCode
方法
hashCode
方法的效果類似於重寫Object
類的hashCode
方法。
/**
* 返回容器對應的hash值
*/
public synchronized int hashCode() {
/*
* This code detects the recursion caused by computing the hash code
* of a self-referential hash table and prevents the stack overflow
* that would otherwise result. This allows certain 1.1-era
* applets with self-referential hash tables to work. This code
* abuses the loadFactor field to do double-duty as a hashCode
* in progress flag, so as not to worsen the space performance.
* A negative load factor indicates that hash code computation is
* in progress.
*/
int h = 0;
if (count == 0 || loadFactor < 0)
return h; // Returns zero
//
loadFactor = -loadFactor; // Mark hashCode computation in progress
Entry<?,?>[] tab = table;
// 將所有entry對應的hashCode求和
for (Entry<?,?> entry : tab) {
while (entry != null) {
h += entry.hashCode();
entry = entry.next;
}
}
loadFactor = -loadFactor; // Mark hashCode computation complete
return h;
}
2、rehash
方法
rehash
方法的作用是進行擴容,並且重新計算key對應的index(調整key-value的位置),此方法雖沒有使用synchronized
關鍵字修飾,但是使用了protected
關鍵字修飾,只對類中方法、子類方法可見,只要調用此方法的方法使用synchronized
關鍵字修飾,那麼也不會引發線程不安全的狀況。
/**
* 容器擴容,並且調整各個key-value的位置
*/
@SuppressWarnings("unchecked")
protected void rehash() {
// 記錄之前table數組的狀態
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 擴大爲原來的2倍 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 如果擴大後的數量超過了容器類定義的最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果擴容前就是最大容量,則無法擴容了
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
// 否則擴容爲最大值
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
// 擴容也是結構性調整
modCount++;
// 計算擴容後key-value的閾值,newCapacity * 負載因子
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 將之前的table數組中的key-value進行調整
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
、HashMap
容器區別
1、HashMap
允許key
和value
爲空,而Hashtable
不允許。
2、Hashtable
是線程安全的(通過synchronized
關鍵字,this鎖
實現),HashMap
不是線程安全。
3、Hashtable
繼承自Dictionary
,HashMap
繼承自AbstractMap
。
4、迭代器不同,Hashtable
是enumerator
迭代器,HashMap
是Iterator
迭代器。
5、Hashtable
中的hash桶由鏈表
構成,HashMap
中的hash桶
可由紅黑樹
或鏈表
構成。
6、Hashtable
中的hash值由Object.hashCode()& 0x7FFFFFFF方法
計算而得(與上0x7FFFFFFF是防止hashCode
出現負數,求餘時也會是負數),HashMap
中的hash
值由Object.hashCode()
的高16位、低16位異或計算而得。
7、Hashtable
初始化默認大小是11,擴容爲原來的2倍+1,HashMap
初始化默認大小是16,每次擴容後的大小都是2的次冪。
九、總結
Hashtable
線程安全,相對效率低,結構也較爲簡單、HashMap
線程不安全,但是高效,不過結構也更復雜些。