Hashtable 算是平時用的比較少的一個集合了,先從繼承、實現關係 及 構造函數來簡單的瞭解一下
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
/**
* The hash table data.
*/
private transient Entry<?,?>[] table;
/**
* Hashtable 中實體數量
*/
private transient int count;
// 擴容閥值
private int threshold;
// 負載因子
private float loadFactor;
// 最大長度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 修改 Hashtable 的次數
private transient int modCount = 0;
// 自定義初始化容量及負載因子構造器
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);
}
// 初始化容量 構造器,默認負載因子 0.75f
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 無參構造器 , 初始化容量 11 負載因子 0.75f
public Hashtable() {
this(11, 0.75f);
}
// Map 構造器 初始化容量 以 2倍的Map 與 11 誰大取誰,負載因子 0.75f
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
Hashtable 繼承了 Dictionary ,我們都知道 Dictionary 的意思是字典,那麼Hashtable 繼承了Dictionary 會不會使其有字典的特性呢? 我們先把這個問題放在這裏。全局變量 無非就是定義一些 擴容閥值 threshold 、負載因子 loadFactor、修改次數 modCount等,具體代表的含義已經在字段上註釋的很清晰了。
對於支持的構造函數,我看可以看到所有的構造函數最終都是調用的 包含對Hashtable的容量大小及負載因子的構造函數。重點看兩個,其一是無參構造函數,從這裏面我們看到Hashtable 的默認大小爲 11 負載因子爲 0.75f 。其二是Map構造函數,其實就是將Map中的所有實體元素 添加到Hashtable中,在此構造函數中,默認的負載因子仍爲0.75f,Hashtable 容量大小爲 Map 中元素個數的 2 倍 與默認大小 11 中取大值作爲初始化容量。
以下我們仍以無參構造爲例,通過put 及remove 兩個函數來了解 Hashtable 的數據結構。
一、put() 函數
同樣我們先看源碼,便於瞭解
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;
}
put 函數由 synchronized 修飾,說明 此函數在多線程環境下是安全的。源碼中第三行對value的判斷及第九行 獲取key的hash值,說明在Hashtable中 key 與 value 均不可爲 null 。源碼第十行,通過key的hash值計算出在Hashtable應該存儲的位置,通過下標index獲取當前位置上的元素,如果當前位置沒有元素,那麼就直接放進去即可。因爲在複雜的業務中 Hashtable 中的元素衆多,發生hash碰撞是經常的事情,那麼在 Hashtable 中是怎麼處理 hash 碰撞的呢,我們可以從 addEntry 中看出端倪:
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
上面對擴容閥值的判斷很好理解,就不一步步梳理了。直接看到最後,先拿到下標爲index的元素,然後創建一個新的實體(也可以叫做元素)並賦值給 當前下標。那麼到目前爲止,還沒有看到處理hash碰撞的具體過程,唯一的處理過程就可能在創建新的元素中了,我們繼續進入創建元素的函數一觀究竟:
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;
}
}
從創建新的元素構造函數我們可以清楚地看到,新創建的元素放置在下標爲index的位置,而之前位於index的元素則被賦值給 新元素的next屬性。從這裏我們可以看到 所有的發生 hash 碰撞的元素,除了放置於index位置的元素,其他元素都會被另一個元素的next 所指向,這不就是一個單向鏈表嗎?而每次由新的元素髮生 hash 碰撞的時候 都會插入到這個單向鏈表的頭部。由於Hashtable 初始化時 帶層維護的是一個數組,所以我們可以知道 Hashtable 就是一個 數組 + 單向鏈表的 數據結構。
從 addEntry 函數中可以看到,如果 count 大於或者等於 threshold 是 會進行一次 rehash 也就是我們理解的擴容操作,具體擴容的方式是怎樣的呢,我們還是從源碼中找到對應的邏輯:
@SuppressWarnings("unchecked")
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;
}
}
}
重點就在第七行,擴容的的邏輯爲 原容量大小的 2倍加1 ,及 newCapacity = 2 * oldCapacity + 1 。
二、remove() 函數
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)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
remove 函數就比較簡單了,源碼看起來也是非常簡明的,首先根據key獲取對應的hash值,然後獲取在Hashtable 中的下標,獲取下標爲index的單向連表,從頭部開始,獲取對應的元素,並將對應元素的父元素的next 指向 對應元素的next,呢麼該元素就相當於從這個鏈表中脫離掉,以達到刪除的目的。如果沒有找到對應的元素則不做任何操作。
整個鏈表的數據結構相信應該十分清楚了,最後附一張結構圖,以增加理解。