HashTable原理以及源碼解析(通俗易懂)
UML圖
概念
HashTable也是一個散列表,它存儲的內容是鍵值對映射。HashTable繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。HashTable的函數都是同步的,這意味着它是線程安全的。它的Key、Value都不可以爲null。此外,HashTable中的映射不是有序的。
HashTable的實例有兩個參數影響其性能:初始容量和加載因子。容量是哈希表中桶的數量,初始容量就是哈希表創建時的容量。注意,哈希表的狀態爲open:在發生“哈希衝突”的情況下,單個桶會存儲多個條目,這些條目必須按順序搜索。加載因子是對哈希表在其容量自動增加之前可以達到多滿的一個尺度。初始容量和加載因子這兩個參數只是對該實現的提示。關於何時以及是否調用rehash方法的具體細節則依賴於該實現。通常,默認加載因子是0.75。
源碼分析
比較重要的參數
private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
table
爲一個Entry[]數組類型,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
count
HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。
threshold
Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值=“容量*加載因子”。
loadFactor
加載因子。
modCount
用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他線程對其進行結構性的修改,這時迭代器會立馬感知到,並且立即拋出ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)
構造方法
1.默認構造函數,容量爲11,加載因子爲0.75
public Hashtable() {
this(11, 0.75f);
}
2.用指定初始容量和默認的加載因子 (0.75) 構造一個新的空哈希表
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
3.用指定初始容量和指定加載因子構造一個新的空哈希表。
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數組
table = new Entry<?,?>[initialCapacity];
//計算閥值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
private int hash(Object k) {
return hashSeed ^ k.hashCode();
}
4.構造一個與給定的 Map 具有相同映射關係的新哈希表
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
幾個常用的方法
1.put方法
//獲取synchronized鎖
public synchronized V put(K key, V value) {
// Make sure the value is not null
//如果value是空拋出異常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//計算key的哈希值和index
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;
}
步驟
1.獲取synchronized鎖。
2.put方法不允許null值,如果發現是null,則直接拋出異常。
3.計算key的哈希值和index
4.遍歷對應位置的鏈表,如果發現已經存在相同的hash和key,則更新value,並返回舊值。
5.如果不存在相同的key的Entry節點,則調用addEntry方法增加節點。
2.addEntry(hash, key, value, index)
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++;
}
步驟
- 當前容量超過閾值,需要擴容
- 生成一個新結點, 將新結點插到鏈表首部
3.rehash()
(擴容方法)相當於hashmap中的resize()方法
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//擴容擴爲原來的兩倍+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++;
//計算下一次rehash的閾值
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;
}
}
}
步驟
1.數組長度增加一倍(如果超過上限,則設置成上限值)。
2.更新哈希表的擴容門限值。
3.遍歷舊錶中的節點,計算在新表中的index,插入到對應位置鏈表的頭部。
4.get方法
public synchronized V get(Object key) {//根據鍵取出對應索引
Entry tab[] = table;
int hash = hash(key);//先根據key計算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//再根據hash值找到索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍歷entry鏈
if ((e.hash == hash) && e.key.equals(key)) {//若找到該鍵
return e.value;//返回對應的值
}
}
return null;//否則返回null
}
步驟
1.先獲取synchronized鎖。
2.計算key的哈希值和index。
3.在對應位置的鏈表中尋找具有相同hash和key的節點,返回節點的value。
4.如果遍歷結束都沒有找到節點,則返回null。
5.remove方法
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//計算key的哈希值和index
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++;
//更新前驅節點的next,指向e的next
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
//返回待刪除節點的value值
return oldValue;
}
}
//如果不存在,返回null
return null;
}
步驟
1.先獲取synchronized鎖。
2.計算key的哈希值和index。
3.遍歷對應位置的鏈表,尋找待刪除節點,如果存在,用e表示待刪除節點,pre表示前驅節點。如果不存在,返回null。
4.更新前驅節點的next,指向e的next。返回待刪除節點的value值。
如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!