什麼是hashtable
HashTable同樣是基於雜湊表實現的,其實類似HashMap,只不過有些區別,HashTable同樣每個元素是一個key-value對,其內部也是通過單連結串列解決衝突問題,容量不足(超過了閥值)時,同樣會自動增長。
HashTable比較古老, 是JDK1.0就引入的類,而HashMap 是 1.2 引進的 Map 的一個實現。
HashTable 是執行緒安全的,能用於多執行緒環境中。Hashtable同樣也實現了Serializable介面,支援序列化,也實現了Cloneable介面,能被克隆。
Hashtable成員變數
private transient Entry[] table;
// Hashtable中元素的實際數量
private transient int count;
// 閾值,用於判斷是否需要調整Hashtable的容量(threshold = 容量*載入因子)
private int threshold;
// 載入因子
private float loadFactor;
// Hashtable被改變的次數
private transient int modCount = 0;
複製程式碼
count是Hashtable的儲存大小,是Hashtable儲存的鍵值對的數量。
threshold是Hashtable臨界值,也叫閥值,如果Hashtable到達了臨界值,需要重新分配大小。閥值 = 當前陣列長度✖負載因子。預設的Hashtable中table的大小為11,負載因子的預設值為0.75。
loadFactor是負載因子, 預設為75%。
modCount指的是Hashtable被修改或者刪除的次數總數。用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他執行緒對其進行結構性的修改,這時迭代器會立馬感知到,並且立即丟擲ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)。
Hashtable的基本原理
從下面的程式碼中我們可以看出,Hashtable中的key和value是不允許為空的,當我們想要想Hashtable中新增元素的時候,首先計算key的hash值,然
後通過hash值確定在table陣列中的索引位置,最後將value值替換或者插入新的元素,如果容器的數量達到閾值,就會進行擴充。
原始碼分析
構造方法
//預設建構函式,容量為11,負載因子是0.75
public Hashtable() {
this(11, 0.75f);
}
//用指定初始容量和預設的載入印在(0.74)構造一個空的雜湊表。
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
//用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化
hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。
這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突:
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,獲得大小為initialCapacity的table陣列
//這裡是與HashMap的區別之一,HashMap中table
table = new Entry[initialCapacity];
//計算閥值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//初始化HashSeed值
initHashSeedAsNeeded(initialCapacity);
}
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
複製程式碼
put方法
public synchronized V put(K key, V value) {//這裡方法修飾符為synchronized,所以是執行緒安全的。
// 確保value不為null
if (value == null) {
throw new NullPointerException();//value如果為Null,丟擲異常
}
Entry tab[] = table;
//計算key的hash值,確認在table[]中的索引位置
int hash = hash(key);
//hash裡面的程式碼是hashSeed^key.hashcode(),null.hashCode()會丟擲異常,所以這就解釋了
Hashtable的key和value不能為null的原因。
int index = (hash & 0x7FFFFFFF) % tab.length;
//獲取陣列元素下標,先對hash值取正,然後取餘。
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
//迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值
V old = e.value;
e.value = value;
return old;
}
}
modCount++;//修改次數。
if (count >= threshold) {//鍵值對的總數大於其閥值
rehash();//在rehash裡進行擴容處理
tab = table;
hash = hash(key);
//hash&0x7FFFFFFF是為了避免負值的出現,對newCapacity求餘是為了使index
在陣列索引範圍之內
index = (hash & 0x7FFFFFFF) % tab.length;
}
//在索引出插入一個新的節點
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
//容器中元素+1 ;
count++;
return null;
}
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();//在1.8的版本中,hash就直接為k.hashCode了。
}
複製程式碼
put方法的流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。
當程式試圖將一個key-value對放入HashMap中時,程式首先根據該 key的 hashCode() 返回值決定該 Entry 的儲存位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的儲存位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新新增 Entry 的 value 將覆蓋集合中原有 Entry的 value,但key不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新新增的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新新增的 Entry 位於 Entry 鏈的頭部
get方法
public synchronized V get(Object key) {
//沒有什麼特殊性,就是加了一個synchronized,就是根據index來遍歷索引處的單連結串列。
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
複製程式碼
相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table陣列中的索引位置,然後迭代連結串列,匹配直到找到相對應key的value,若沒有找到返回null。、rehash方法
HashTable的擴容操作,在put方法中,如果需要向table[]中新增Entry元素,會首先進行容量校驗,如果容量已經達到了閥值,HashTable就會進行擴容處理rehash()
protected void rehash() {
int oldCapacity = table.length;
//元素
Entry<K,V>[] oldMap = table;
//新容量=舊容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//這裡的最大值和HashMap裡的最大值不同,這裡Max_ARRAY_SIZE的是
因為有些虛擬機器實現會限制陣列的最大長度。
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
//新建一個size = newCapacity 的HashTable
Entry<K,V>[] newMap = new Entry[];
modCount++;
//重新計算閥值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//重新計算hashSeed
boolean rehash = initHashSeedAsNeeded(newCapacity);
table = newMap;
//將原來的元素拷貝到新的HashTable中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
if (rehash) {
e.hash = hash(e.key);
}
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
} 複製程式碼
在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經變了。:比如初始值11、載入因子預設0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,一次類推。