在 Java 開發中,Hashtable 與 HashMap 是兩個最基礎且常用的鍵值對存儲容器。雖然它們都實現了 Map 接口,底層都基於哈希表數據結構,但在線程安全性、空值處理、迭代器機制、擴容策略等關鍵特性上存在顯著差異。本文將從源碼層面深度剖析兩者的異同,幫助開發者根據業務場景做出正確的技術選型。
| 特性 | Hashtable | HashMap |
|---|---|---|
| 線程安全 | 同步(所有 public 方法均加 synchronized) | 非同步(需外部手動同步或使用 ConcurrentHashMap) |
| 允許 Null | Key 和 Value 都不允許為 null | 允許一個 null Key、多個 null Value |
| 迭代器 | 使用 Enumeration(fail-safe) | 使用 Iterator(fail-fast) |
| 繼承體系 | 繼承 Dictionary 類 | 繼承 AbstractMap 類 |
| 初始容量 | 默認 11 | 默認 16(且必須為 2 的冪次) |
| 擴容機制 | 新容量 = old * 2 + 1 | 新容量 = old * 2(保持 2 的冪次) |
| 哈希計算 | 直接使用對象的 hashCode() | 對 hashCode 進行二次擾動計算,再用位運算替代取模 |
Hashtable 的所有公共方法(如 put、get、remove)均聲明為 synchronized,這意味著同一時間只有一個線程能訪問其內部數據結構。雖然保證了線程安全,但在高併發場景下性能極差,因為所有操作都串行化了。
HashMap 則完全無鎖,多線程環境下會導致數據不一致甚至死循環(JDK 1.7 擴容鏈表倒序問題)。若需線程安全,推薦使用 ConcurrentHashMap(JDK 1.8 基於 CAS + synchronized + 紅黑樹,分段鎖粒度更細)或 Collections.synchronizedMap(new HashMap<>())(全表鎖,性能接近 Hashtable)。
Hashtable 的 put 方法中顯式檢查 key == null || value == null 並拋出 NullPointerException。這源於其早期設計決策:無法區分「鍵不存在」與「鍵對應值為 null」的語義歧義。而 HashMap 允許一個 null Key(存放於 table[0] 位置),通過 get(key) 返回 null 時,可進一步調用 containsKey(key) 區分兩種情況。
modCount 修改計數,若發現結構性修改(如 put/remove),立即拋出 ConcurrentModificationException,屬於「快速失敗」機制,幫助開發者及早發現併發Bug。HashMap 引入了 hash(Object key) 擾動函數(高 16 位與低 16 位異或),降低哈希碰撞概率;並利用 (n - 1) & hash 位運算替代取模運算,前提是數組長度必須為 2 的冪次。擴容時直接左移一位(乘 2),無需重新計算模數,極大提升性能。
Hashtable 直接使用 key.hashCode() 取模 table.length,擴容公式 old * 2 + 1 保證容量為奇數,雖然一定程度上減少碰撞,但取模運算效率低於位運算,且擴容後需重新計算所有鍵的索引位置,開銷較大。
Hashtable<String, Integer> numbers = new Hashtable<>();
numbers.put("one", 1); // 自動裝箱
numbers.put("two", 2);
numbers.put("three", 3);
// 獲取值
Integer n = numbers.get("two");
System.out.println("two = " + n); // 輸出: two = 2
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", null); // 允許 null Value
scores.put(null, 100); // 允許一個 null Key
// 安全遍歷(推薦 forEach + Lambda)
scores.forEach((k, v) -> System.out.println(k + " -> " + v));
// 並發場景下的正確選擇
ConcurrentMap<String, Integer> concurrentScores = new ConcurrentHashMap<>();
concurrentScores.put("Charlie", 88);
ConcurrentHashMap。new HashMap<>(expectedSize),避免頻繁擴容導致性能抖動。負載因子默認 0.75 通常無需調整。a.equals(b) == true 推導出 a.hashCode() == b.hashCode(),否則會導致「鍵明明存在卻取不到值」的離譜 Bug。remove() 方法,直接調用 Map 的 remove 會拋出異常。Hashtable 是 Java 1.0 遺留的產物,其同步機制粗暴、API 陳舊、性能低下,已被 ConcurrentHashMap 徹底取代。HashMap 憑藉非同步高性能、靈活的空值支持、優秀的哈希擾動算法與紅黑樹優化(JDK 1.8 鏈表長度超 8 轉紅黑樹),成為單線程環境下鍵值存儲的首選。理解兩者底層差異,不僅是面試必考點,更是寫出高質量 Java 代碼的基石。