淺談 Hash、Hashtable 與 HashMap
Hash 這玩意兒是程式世界的基礎,但一堆人搞不清楚 Hash、Hashtable、HashMap 的差別。常見情況:被問到為啥用 HashMap 不用 Hashtable,只能兩眼發直。今天就一次講清楚。
Hash 的概念
Hash 就是把任意長度的資料「打碎」再「拼湊」成固定長度的代碼。
Hash Function 做的事
1 | String input = "This is a very long sentence with lots of words"; |
Hash Function 的特性:
- 決定性 - 同一個輸入永遠產生同一個輸出
- 單向性 - 不可逆,不能從 hash code 推回原始資料
- 雪崩效應 - 輸入改一點點,輸出就完全不同
- 均勻分佈 - 理想情況下,不同輸入的 hash 值均勻分佈在整個範圍
Hash 的應用
1. 數位簽章
確認資料沒有被竄改。簽署人計算文件的 hash 值,用私鑰加密。驗證人計算一遍 hash,再用公鑰解密比對。如果兩個 hash 相同,代表文件沒動過。
2. 資料完整性驗證
下載檔案時,官方會提供 SHA256 checksum。你下載完自己算一次,比對是否相同。如果相同,代表沒有傳輸損壞或被中間人修改。
3. 密碼儲存
千萬別存明文密碼。應該存 hash 值。使用者登入時,計算輸入密碼的 hash,和資料庫裡的比對。即使資料庫被盜,駭客也只能拿到 hash,推不回密碼。
編碼 vs 加密 vs Hash
很多人搞混這三個:
1 | 編碼(Encoding):ASCII、UTF-8、Base64 |
Hash Table 資料結構
Hash Table 是一種資料結構,用來解決「快速查找」問題。
基本概念
傳統陣列的問題:
- 有序查找(線性搜尋):O(n),要一個一個比
- 二元搜尋:O(log n),但需要排序
Hash Table 用 hash code 當 index:
1 | 要存 key="Alice", value="Engineer" |
理想情況下,查詢、插入、刪除都是 O(1)。
雜湊碰撞問題
但現實有個問題:雜湊碰撞(hash collision)。
1 | hash("Alice") = 5 |
不同的鍵產生相同的 hash code,怎麼辦?有兩個常見解法:
1. Closed Addressing(閉放定址法)
在 table[5] 這個位置放一個 linked list 或其他結構,把所有碰撞的元素都存進去:
1 | table[5] -> "Alice" -> "Cathy" -> null |
查詢時要遍歷 linked list,最壞情況 O(n)。但如果 hash function 夠好,碰撞很少,平均還是接近 O(1)。
2. Open Addressing(開放定址法)
碰撞時,找個 empty slot 放進去:
1 | hash("Alice") = 5 -> table[5] |
線性探測、二次探測等方法。
Java 的 Hashtable 類
Hashtable 是 JDK 1.0 的遠古生物。
基本特性
1 | Hashtable<String, Integer> ht = new Hashtable<>(); |
主要特性:
- 執行緒安全 - 幾乎所有方法都加了
synchronized - Key 和 Value 都不能是 null - put(null, value) 直接 NPE
- 效能一般 - synchronized 全都上,競爭激烈時效能堪憐
- 舊時代遺留 - 現在已經棄用
為啥不允許 null?因為 Hashtable 無法區分「鍵不存在」和「鍵對應的值是 null」。get() 回傳 null 時,你不知道是找不到,還是真的存了 null。
為啥要棄用
效能問題太嚴重。簡單的 get 操作:
1 | public synchronized V get(Object key) { |
整個方法都 synchronized,其他執行緒都要排隊。如果你有 100 個執行緒都在讀,全部堵住。
HashMap 類
HashMap 是現代的選擇。
基本特性
1 | Map<String, Integer> map = new HashMap<>(); |
主要特性:
- 不執行緒安全 - 沒有 synchronized,多執行緒要自己同步或用 ConcurrentHashMap
- Key 和 Value 可以是 null - 但只能有一個 null key
- 效能好 - 沒有鎖的開銷
- 可預測的迭代順序 - 不保證(1.7 是 bucket order,1.8+ 是 insertion order for linked entries)
HashMap 內部結構(Java 1.8+)
1 | // 簡化版結構 |
Java 1.8 之後的改進:
- 碰撞少時用 linked list
- 碰撞多時(超過 8 個)自動轉成 red-black tree,查詢快速到 O(log n)
什麼時候會 resize
1 | HashMap<String, Integer> map = new HashMap<>(); // 容量 16 |
Resize 很昂貴(要重新計算所有元素的 index),所以如果預先知道大小,最好初始化時指定:
1 | Map<String, Integer> map = new HashMap<>(1000); // 指定容量 1000 |
ConcurrentHashMap 是更好的選擇
需要執行緒安全?別再用 Hashtable,用 ConcurrentHashMap:
1 | Map<String, Integer> map = new ConcurrentHashMap<>(); |
優勢:
- 分段鎖(Segment Lock) - 不是鎖整個表,而是分成多個段,每個段有自己的鎖
- 好效能 - 多執行緒讀寫競爭不大
- Key 和 Value 不能是 null
簡單概念:
1 | Hashtable: |
Java Collections Framework
Hashtable 和 HashMap 在 Java Collections 的位置不同。
1 | Collection(介面) |
重點:Map 和 Collection 並列,不是包含關係。
這是因為 Map 是 key-value 結構,邏輯上和 Collection(單一元素)不同。
實務建議
新專案用 HashMap - 除非特殊需求,Hashtable 已死
需要執行緒安全用 ConcurrentHashMap
1
2
3
4
5// 不好
Map<String, Data> map = Collections.synchronizedMap(new HashMap<>());
// 更好
Map<String, Data> map = new ConcurrentHashMap<>();保持插入順序用 LinkedHashMap
1
Map<String, Integer> map = new LinkedHashMap<>(); // 保持插入順序
預先知道大小,初始化容量
1
Map<String, String> map = new HashMap<>(100); // 避免頻繁 resize
永遠別用 null 作為 key - 會造成混亂
1
2
3
4// 不好做法
map.put(null, "value");
if (map.get(someKey) == null) { // 不知道是 null key 還是 not found
}
就醬,Hash 的世界沒那麼複雜,關鍵是理解基本概念,選對工具。










