Java 集合框架深度解析:Hashtable 核心原理、遍歷方式與 HashMap 關鍵差異對比
Hashtable 是 Java 1.0 時代引入的經典哈希表實現,雖然被 HashMap 和 ConcurrentHashMap 取代主流地位,但理解其內部結構、同步機制與局限性,有助於深入掌握集合框架演進脈絡。本文系統剖析 Hashtable 源碼(JDK 8 視角),覆蓋數據結構、核心方法、遍歷方式、與 HashMap 的關鍵差異。
一、類定義與繼承體系
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
- 繼承
Dictionary<K,V>:古老抽象類,已被 Map 取代
- 實現
Map、Cloneable、Serializable
- 所有公共方法均加
synchronized:方法級粗粒度同步
二、核心數據結構:拉鏈法哈希表
// 核心字段
private transient Entry<K,V>[] table; // 哈希桶數組
private transient int count; // 元素數量
private int threshold; // 擴容閾值 = capacity * loadFactor
private float loadFactor; // 加載因子 (默認 0.75)
private transient int modCount = 0; // 結構修改次數,支撐 fail-fast
Entry 節點(單向鏈表)
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next; // 指向下一個節點
Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash; this.key = key; this.value = value; this.next = next;
}
// getKey/getValue/setValue 實現...
}
三、構造方法族系
| 構造方法 |
默認容量 |
默認加載因子 |
說明 |
Hashtable() |
11 |
0.75 |
最常用,質數容量減少碰撞 |
Hashtable(int initialCapacity) |
指定 |
0.75 |
預估數據量時使用 |
Hashtable(int initialCapacity, float loadFactor) |
指定 |
指定 |
完全控制 |
Hashtable(Map<? extends K,? extends V> t) |
max(2*t.size(), 11) |
0.75 |
複製現有 Map |
四、核心方法源碼剖析
1. put(K key, V value) —— 同步插入/更新
public synchronized V put(K key, V value) {
// 1. 值不能為 null(關鍵差異)
if (value == null) throw new NullPointerException();
// 2. 計算索引
Entry<K,V>[] tab = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
// 3. 遍歷鏈表:key 存在則更新,返回舊值
for (Entry<K,V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
// 4. 擴容檢查
modCount++;
if (count >= threshold) {
rehash(); // 容量翻倍+1,重新哈希
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 5. 頭插法插入新節點
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;
}
2. get(Object key) —— 同步查找
public synchronized V get(Object key) {
Entry<K,V>[] 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;
}
3. rehash() 擴容機制
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldTable = table;
// 新容量 = 老容量 * 2 + 1 (保持質數特性)
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE) return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<K,V>[] newTable = new Entry<K,V>[newCapacity];
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newTable;
// 重新哈希所有節點到新表
for (int i = oldCapacity; i-- > 0;) {
for (Entry<K,V> old = oldTable[i]; old != null; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newTable[index];
newTable[index] = e;
}
}
}
五、四種遍歷方式(均為同步安全)
Hashtable<String, String> table = new Hashtable<>();
// ... 放入數據
// 1. keys() - Enumeration (舊 API)
Enumeration<String> en1 = table.keys();
while (en1.hasMoreElements()) { String k = en1.nextElement(); }
// 2. elements() - Enumeration (舊 API)
Enumeration<String> en2 = table.elements();
while (en2.hasMoreElements()) { String v = en2.nextElement(); }
// 3. keySet() - Iterator (現代 API)
Iterator<String> it1 = table.keySet().iterator();
while (it1.hasNext()) { String k = it1.next(); }
// 4. entrySet() - Iterator (現代 API,推薦)
Iterator<Map.Entry<String, String>> it2 = table.entrySet().iterator();
while (it2.hasNext()) {
Map.Entry<String, String> entry = it2.next();
String k = entry.getKey(); String v = entry.getValue();
}
六、Hashtable vs HashMap 關鍵差異對比
| 維度 |
Hashtable |
HashMap (JDK 8) |
影響 |
| 同步機制 |
所有方法 synchronized (粗粒度) |
非同步;多線程用 Collections.synchronizedMap 或 ConcurrentHashMap |
Hashtable 併發性能極差 |
| Null 支持 |
Key/Value 均不允許 null |
允許 1 個 null key、多個 null value |
Hashtable 使用受限 |
| 繼承體系 |
extends Dictionary |
extends AbstractMap |
Hashtable 屬舊體系 |
| 初始容量 |
11 (質數) |
16 (2 的冪次) |
擴容策略不同 |
| 擴容方式 |
2*old + 1 |
2*old (保持 2 的冪次) |
HashMap 位運算更快 |
| 哈希計算 |
自定義 hash(key) (key.hashCode()) |
擾動函數 hash(key) = h ^ (h >>> 16) |
HashMap 碰撞更少 |
| 鏈表轉紅黑樹 |
不支持 |
鏈表長度 ≥ 8 且容量 ≥ 64 轉樹 |
HashMap 極端情況 O(log n) |
| 遍歷順序 |
不確定 |
不確定 (LinkedHashMap 保持插入序) |
同 |
| fail-fast |
modCount 支持 |
modCount 支持 |
同 |
七、為什麼不推薦在新項目中使用 Hashtable?
- 同步粒度太粗:全表鎖,併發吞吐極低
- 功能受限:不支持 null,無法滿足現代業務需求
- 替代方案更優:
- 單線程/非併發 →
HashMap
- 併發讀寫 →
ConcurrentHashMap (JDK 8 CAS + synchronized 細粒度,JDK 17 更強)
- 需要同步 Map 視圖 →
Collections.synchronizedMap(new HashMap<>())
- 僅存於遺留代碼:早期 API、部分第三方庫、舊配置類
八、總結
- Hashtable 採用數組 + 單向鏈表(拉鏈法),質數容量、頭插法、全方法同步
- 核心限制:Key/Value 不可為 null,這是與 HashMap 最大行為差異
- 擴容策略
2n+1 保持質數,但位運算不如 2 的冪次高效
- 現代開發全場景替換為 HashMap / ConcurrentHashMap
- 理解 Hashtable 有助於掌握哈希表演進:從粗粒度鎖 → 細粒度鎖 → 無鎖 CAS → 紅黑樹優化