Java容器——Hashtable(Java9)原始碼解析
Hashtable是一種鍵值對型Java儲存容器,自JDK1.0沿用至今。經常有將Hashtable和HashMap進行比較的例子和文章,實際上早期二者的實現原理基本一致,而HashTable的操作方法都進行了加鎖,因而執行緒安全。本文從原始碼角度介紹HashTable的實現。
一 組成元素
1 關鍵變數
/** * Hashtable bucket collision list entry */ 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; } ... // Map.Entry Ops public K getKey() { return key; } public V getValue() { return value; } 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; return (key==null ? e.getKey()==null : key.equals(e.getKey())) && (value==null ? e.getValue()==null : value.equals(e.getValue())); } public int hashCode() { return hash ^ Objects.hashCode(value); } }
前面我們說到,Hashtable是儲存鍵值對的容器,Entry<K,V>這個內部類就是實現鍵值對的最小組成元素。如執行下面的這段程式碼 Hashtable<String, Integer> numbers = new Hashtable<String, Integer>();numbers.put("one", 1); ("one",1)就組成了<String,Integer>的Entry。而整個Hashtable的操作實際上也就是Entry的增刪改查等的操作,歸根到底,最需要關注的是Entry的儲存方式,這樣才能理解各個操作的步驟和含義。
/** * The hash table data. */ private transient Entry<?,?>[] table; /** * The total number of entries in the hash table. */ private transient int count; /** * The table is rehashed when its size exceeds this threshold. (The * value of this field is (int)(capacity * loadFactor).) */ private int threshold; /** * The load factor for the hashtable. */ private float loadFactor;
Hashtable實際上是一個一維陣列,也就是table[],陣列元素是以Entry為組成單元的單向連結串列。變數count顯示了當前的Hashtable中的元素個數。threshold代表了Hashtable需要擴容時的數量閾值,loadFactor是擴容的百分比閾值。
Hashtable的構造如上圖所示,每一個白色框代表了一個table元素,存放的是單向連結串列的首個元素,後繼元素按順序排列。
2 建構函式
/**
* Constructs a new, empty hashtable with the specified initial
* capacity and the specified load factor.
*
* @param initialCapacity the initial capacity of the hashtable.
* @param loadFactor the load factor of the hashtable.
* @exception IllegalArgumentException if the initial capacity is less
* than zero, or if the load factor is nonpositive.
*/
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;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
/**
* Constructs a new, empty hashtable with the specified initial capacity
* and default load factor (0.75).
*
* @param initialCapacity the initial capacity of the hashtable.
* @exception IllegalArgumentException if the initial capacity is less
* than zero.
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
/**
* Constructs a new hashtable with the same mappings as the given
* Map. The hashtable is created with an initial capacity sufficient to
* hold the mappings in the given Map and a default load factor (0.75).
*
* @param t the map whose mappings are to be placed in this map.
* @throws NullPointerException if the specified map is null.
* @since 1.2
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
Hashtable的建構函式指定了兩個關鍵變數,初始容量,和承載因子。承載因子被設定為0.75,按照官方文件的說明,是綜合了時間和空間的利用效率的經驗值。
二 函式概述
1 關鍵函式
Hashtable的優勢在於通過hash計算下標,可以以常數時間查詢元素。這裡會有三個問題
- 怎麼計算下標
- 如果下標重疊了如何處理
- 何時擴容,如何擴容
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
首先計算Key的雜湊值,然後對下標陣列取餘找到陣列下標。前面我們說到,Hashtable是單鏈表的陣列,出現雜湊碰撞的情況,就在該下標所儲存的連結串列中遍歷查詢需要操作的元素位置進行後續操作。
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
...
}
}
當Hashtable儲存了超過threshold的Entry(count / capacity >= loadFactor),就需要對Hashtable進行擴容,這也是Hashtable常用的關鍵步驟。
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
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++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
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;
}
}
}
如rehash所描述的,首先對儲存陣列進行擴容,方式是原陣列長度的二倍加一,並建立新的table。接下來就要將存在原table中的各個連結串列轉移到新table中。具體操作如下
1 遍歷原table,找到一條連結串列的首元素;
2 計算該元素在新table中的index,將該元素的next指標指向newtable[index],並以此元素為該槽位的首元素;
3 後續元素依次遍歷處理。
這裡可能需要注意的地方是步驟2,不同於單向連結串列的末尾新增元素,這裡是每次在隊首新增元素,避免了遍歷該單鏈表。
2 增刪改查
理解一個容器關鍵在於理解容器元素的存放方式。前面我們說明了Hashtable的構成和儲存方式,下面列舉的增刪改查實際上都是針對這種結構型別的操作。而Hashtable的增刪改查,其共性在於查,找到元素了才可以進行下一步的操作。以查詢元素為例。先找下標,找到目標table[index]後,再逐個遍歷連結串列元素直到找到目標。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
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;
}
}
return null;
}
對於新增操作,首先是找到了要新增元素Key的雜湊對應的table的下標,如果為空則新增為連結串列頭,如果有元素則新增到該連結串列的末尾。新增和刪除元素也是同理,先找到元素的位置,然後進行連結串列的新增和刪除操作。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
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;
}
}
addEntry(hash, key, value, index);
return null;
}
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];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
modCount++;
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
理解了增刪改查的基本操作,對於Hashtable的用法和原理也就基本都瞭解了。
三 小結
本文對Hashtable的組成和基本操作進行了介紹,行文至此,有一個問題很容易提出,Hashtable自JDK1.0就存在,為何現在使用率越來越低?為何越來越多轉而使用HashMap或ConcurrentHashMap?
筆者列出幾個因素拋磚引玉:
1 Hashtable計算table下標的方式簡單粗暴,直接使用hash對table長度取餘,如果table長度較短,或Hash值末尾幾位相同,那麼將有多個元素存放於一個table的槽內。
2 對於Hash碰撞的,Hashtable採用單鏈表形式存放元素,對這些元素的各種操作都需要對單鏈表進行遍歷,效率低下,完全喪失了Hashtable查詢的速度優勢。
3 Hashtable的重要操作包括查詢都是synchronized操作,保證或了執行緒安全但同時非常耗時。
那麼,常用的HashMap和同步的ConcurrentHashMap是如何解決這些問題的呢?讀者可以閱讀相應原始碼理解一探究竟。