目錄
一、概述
- Hashtable也稱爲散列表,它存儲的內容是鍵值對(key-value)映射,是根據關鍵字值(key value)直接進行訪問的數據結構。也就是說,它通過把關鍵字值映射到一個位置來訪問記錄,以加快查找的速度。這個映射函數稱爲哈希函數(也稱爲散列函數),映射過程稱爲哈希化,存放記錄的數組叫做散列表。
- Hashtable繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。
- Hashtable的函數都是同步的,這意味着它是線程安全的。它的key、value都不可以爲null,Hashtable中的映射不是有序的。
- Hashtable的實例有兩個參數影響其性能: ①初始容量(initialCapacity) :哈希表中桶 的數量,初始容量 就是哈希表創建時的容量; ②負載因子(load Factor):負載因子哈希表是0.1 到 1.0 範圍內的數字,當容量自動增加(擴容rehash)之前允許哈希表得到滿足的度量。初始容量和負載因子這兩個參數只是對該實現的提示。通常,默認負載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。負載因子過高雖然減少了空間開銷,但同時也增加了查找某個條目的時間。
二、原理(圖解)
三、源碼分析
1、相關參數定義:
/**
* 爲一個Entry[]數組類型,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,哈希表的"key-
value鍵值對"都是存儲在Entry數組中的。
*/
private transient Entry<?,?>[] table;
/**
* 哈希表中條目的總數
*/
private transient int count;
/**
* table數組擴容的節點數閾值,以此來判斷是否達到擴容標準
*/
private int threshold;
/**
* 負載因子默認0.75f
*/
private float loadFactor;
/**
* Hashtable被修改次數,用來實現“fail-fast”機制的(也就是快速失敗)。
*/
private transient int modCount = 0;
2、構造函數:
// 初始化默認構造函數。
public Hashtable() {
//默認容量爲11 負載因子爲0.75
this(11, 0.75f);
}
// 初始化指定“容量大小”的構造函數
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
// 初始化指定“容量大小”和“加載因子”的構造函數
public Hashtable(int initialCapacity, float loadFactor) {
//傳入容量不能<0,否則會報異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
//傳入的加載因子範圍在0——1之間,默認0.75 建議設置在0.7——0.75之間,否則也會報異常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//如果傳入的容量爲0,那麼會默認把容量初始爲1,可看出容量默認爲11,最小爲1
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//創建容量爲initialCapacity的Entry數組table
table = new Entry<?,?>[initialCapacity];
//計算出數組的閥值,也算一個臨界值吧(閥值表示當table的長度達到這個閥值(臨界值)之後就會觸
//發擴容機制(rehash))
//計算閥值的公式:閥值=容量*負載因子 與 當前系統數組最大長度+1 的最小值
//MAX_ARRAY_SIZE =Integer.MAX_VALUE - 8
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 初始化包含“子Map”的構造函數
public Hashtable(Map<? extends K, ? extends V> t) {
// 初始容器取map最大尺寸*2和11取大的,也就是最小也是11,負載因子0.75
this(Math.max(2*t.size(), 11), 0.75f);
//將傳入的map放入數組
putAll(t);
}
//將傳入的map放入數組
public synchronized void putAll(Map<? extends K, ? extends V> t) {
//遍歷循環傳入的map,調用put放入數組
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
//放入
put(e.getKey(), e.getValue());
}
接下來主要分析常用的幾個方法(get / put / remove / rehash) ,在這個之前我們先看下Entry這個類
3、Entry類分析
//Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。
private static class Entry<K,V> implements Map.Entry<K,V> {
//定義一個int類型的hash字段
final int hash;
//定義key字段
final K key;
//定義value字段
V value;
//存儲下一個Entry數組對象
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;
}
//克隆
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// 獲取Entry中的key
public K getKey() {
return key;
}
//獲取Entry中的value值
public V getValue() {
return value;
}
//設置Entry中的value值 可以看出hashtable的key和value都不能爲null,否則會拋異常
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
//比較equals
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()));
}
//獲取hashCode
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
4、get方法
//根據key從哈希表中獲取對應的value值
//獲取方法同步鎖synchronized
public synchronized V get(Object key) {
//創建Entry[]數組類型
Entry<?,?> tab[] = table;
//獲取key的hash值
int hash = key.hashCode();
//再根據hash值和table的長度計算出在table數組中的索引位置
//擴展:hash值爲int類型 4個字節 32bit.
// 爲了在hash爲負值的情況下,去掉起符號位,所以和0x7FFFFFFF進行&操作
// 0x7FFFFFFF 二進制 0111 1111 1111 1111 1111 1111 1111 1111
// 負數與其進行&操作將產生一個正整數
int index = (hash & 0x7FFFFFFF) % tab.length;
//遍歷對應位置的鏈表
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//在鏈表中查找hash值和key值都相等的元素
if ((e.hash == hash) && e.key.equals(key)) {
//返回節點的值
return (V)e.value;
}
}
//沒有找到則返回null
return null;
}
5、put放入哈希表
//添加鍵值對
public synchronized V put(K key, V value) {
//判斷value是否爲空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//先獲取key的hash值
int hash = key.hashCode();
//再根據hash值和table的長度計算出在table數組中的索引位置
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)) {
//若添加的key在Hashtable已經存在,則用新value覆蓋原有的value
V old = entry.value;
entry.value = value;
return old;
}
}
//添加鏈表節點
addEntry(hash, key, value, index);
return null;
}
//添加鏈表節點
private void addEntry(int hash, K key, V value, int index) {
//更改次數加1
modCount++;
Entry<?,?> tab[] = table;
//當哈希表實際容量>=哈希表的閥值(臨界值),觸發擴容操作
if (count >= threshold) {
//進行擴容操作
rehash();
//將擴容後的table賦值給新創建的Entry數組tab[]
tab = table;
//先獲取key的hash值
hash = key.hashCode();
//再根據hash值和table的長度計算出在table數組中的索引位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 創建一個新的Entry數組
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
//哈希表哈希表實際容量+1
count++;
}
6、rehash 擴容解析
//擴容操作
protected void rehash() {
//獲得擴容前數組容量
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
//計算得出新的數組容量,新數組容量=舊數組容量*2+1(<<1表示右移一位 <<1=2的一次方=2)
int newCapacity = (oldCapacity << 1) + 1;
//如果新數組容量>數組規定的最大容量限制,則使用最大限制容器值 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0) {
//如果舊的數組容量=MAX_ARRAY_SIZE則
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
//創建一個容量爲newCapacity的新的數組對象
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
//容器更改次數+1
modCount++;
//獲取新的數組閥值(臨界值)
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//將新的數組賦值給table參數
table = newMap;
//依次循環將原有元素複製到新的Hashtable中
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;
}
}
}
四、Hashtable知識延伸
1)Java中==,equals和hashCode有什麼區別和聯繫?https://www.cnblogs.com/aspirant/p/7079538.html
2)爲什麼加載因子要用0.75? https://www.cnblogs.com/aspirant/p/11470928.html
3)爲什麼獲取數組下標時要 key.hashCode() & 0x7fffffff ?https://blog.csdn.net/weixin_39590058/article/details/88925659
4)爲什麼擴容是2N+1?
5)負載因子值的大小,對HashMap有什麼影響?
6) Hashtable的複雜度爲什麼是O(1)?
7)Hashtable最大容量爲什麼是2^31-8?https://blog.csdn.net/ChineseYoung/article/details/80787071