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是如何解決這些問題的呢?讀者可以閱讀相應源碼理解一探究竟。