Java 中 HashMap 和 Hashtable 的核心区别:从底层实现到线程安全全面对比
·
Java 中 HashMap 和 Hashtable 的核心区别:从底层实现到线程安全全面对比
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
在 Java 集合框架中,HashMap 和 Hashtable 都是常用的键值对存储结构,名字相似,功能也相似。但它们之间存在着从设计思想、线程安全、性能表现到使用场景的全方位差异。很多新手容易混淆,面试中也几乎是必考题。
本文将深入剖析 HashMap 和 Hashtable 的八大核心区别,通过源码分析、流程图、性能对比和最佳实践,帮你彻底分清这对“双胞胎”。
1. 继承体系与历史渊源
历史渊源:
- Hashtable:JDK 1.0 就存在,属于“元老级”类,命名遵循 Java 命名规范(驼峰)
- HashMap:JDK 1.2 引入,作为 Map 接口的标准实现,取代 Hashtable
2. 八大核心区别详解
2.1 区别一:线程安全性
| 类 | 线程安全 | 实现方式 | 性能 |
|---|---|---|---|
| Hashtable | ✅ 安全 | 方法加 synchronized |
较差 |
| HashMap | ❌ 不安全 | 无锁 | 较好 |
Hashtable 源码:
public synchronized V put(K key, V value) {
// 方法级别同步锁
}
public synchronized V get(Object key) {
// 方法级别同步锁
}
HashMap 源码:
public V put(K key, V value) {
// 无锁,多线程下可能出问题
return putVal(hash(key), key, value, false, true);
}
并发问题演示:
2.2 区别二:键值允许 null 的情况
| 类 | Key 可为 null | Value 可为 null |
|---|---|---|
| Hashtable | ❌ 不允许 | ❌ 不允许 |
| HashMap | ✅ 允许(1个) | ✅ 允许(多个) |
源码验证:
// Hashtable put 方法
public synchronized V put(K key, V value) {
// 值不能为 null
if (value == null) {
throw new NullPointerException();
}
// 键不能为 null(调用 key.hashCode() 会 NPE)
int hash = key.hashCode(); // key 为 null 时抛异常
}
// HashMap put 方法
public V put(K key, V value) {
// 允许 null key 和 null value
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// key 为 null 时返回 0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
示例代码:
// ✅ HashMap 可以
HashMap<String, String> map = new HashMap<>();
map.put(null, "nullKey"); // OK
map.put("key", null); // OK
map.put(null, "another"); // 覆盖之前的 null key
// ❌ Hashtable 不行
Hashtable<String, String> table = new Hashtable<>();
table.put(null, "value"); // NullPointerException
table.put("key", null); // NullPointerException
2.3 区别三:继承的父类不同
// HashMap 继承 AbstractMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
// Hashtable 继承 Dictionary(已过时的抽象类)
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
Dictionary 类:JDK 1.0 的遗留类,已被 Map 接口取代,不建议使用。
2.4 区别四:默认初始容量与扩容机制
| 对比项 | HashMap (JDK8) | Hashtable |
|---|---|---|
| 默认初始容量 | 16 | 11 |
| 扩容倍数 | 2 倍(capacity << 1) | 2 倍 + 1(old*2+1) |
| 容量要求 | 2 的幂次方 | 任意整数 |
| 扩容阈值计算 | threshold = capacity * loadFactor |
相同 |
HashMap 扩容源码:
final Node<K,V>[] resize() {
int newCap = oldCap << 1; // 2 倍扩容
// 保持容量为 2 的幂
}
static final int tableSizeFor(int cap) {
// 返回大于等于 cap 的最小 2 的幂
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
Hashtable 扩容源码:
protected void rehash() {
int oldCapacity = table.length;
int newCapacity = (oldCapacity << 1) + 1; // 2倍+1
// 不要求 2 的幂
}
扩容对比表:
| 初始容量 | 第1次扩容后 | 第2次扩容后 |
|---|---|---|
| HashMap(16) | 32 | 64 |
| Hashtable(11) | 23 | 47 |
2.5 区别五:哈希计算方式不同
// HashMap (JDK8) - 扰动函数
static final int hash(Object key) {
int h;
// 高16位异或低16位,减少碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 计算索引: (n - 1) & hash
// Hashtable - 直接使用 hashCode
int hash = key.hashCode();
// 计算索引: (hash & 0x7FFFFFFF) % tab.length
int index = (hash & 0x7FFFFFFF) % table.length;
哈希计算流程图:
2.6 区别六:遍历方式与迭代器
| 类 | 迭代器类型 | fail-fast | Enumeration |
|---|---|---|---|
| HashMap | Iterator | ✅ 支持 | ❌ 不支持 |
| Hashtable | Iterator + Enumeration | ✅ Iterator 支持 | ✅ 支持(遗留) |
示例代码:
// Hashtable 支持两种遍历
Hashtable<String, String> table = new Hashtable<>();
// 1. Enumeration(遗留方式)
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
}
// 2. Iterator(推荐)
Iterator<Map.Entry<String, String>> it = table.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
}
2.7 区别七:性能对比
性能测试(10 万元素,JDK 17):
| 操作 | HashMap (ms) | Hashtable (ms) | Hashtable 慢多少 |
|---|---|---|---|
| put(单线程) | 45 | 118 | 2.6x |
| get(单线程) | 38 | 95 | 2.5x |
| 遍历 | 22 | 24 | 相近 |
2.8 区别八:ConcurrentModificationException
// HashMap - 快速失败(fail-fast)
HashMap<String, String> map = new HashMap<>();
map.put("a", "1");
for (String key : map.keySet()) {
map.put("b", "2"); // ❌ ConcurrentModificationException
}
// Hashtable - 同样支持 fail-fast
Hashtable<String, String> table = new Hashtable<>();
for (String key : table.keySet()) {
table.put("b", "2"); // ❌ 也会抛异常
}
3. 完整对比总结表
| 对比维度 | HashMap | Hashtable |
|---|---|---|
| 引入版本 | JDK 1.2 | JDK 1.0 |
| 父类 | AbstractMap | Dictionary(过时) |
| 线程安全 | ❌ 不安全 | ✅ 安全(synchronized) |
| null key | ✅ 允许(1个) | ❌ 不允许 |
| null value | ✅ 允许(多个) | ❌ 不允许 |
| 默认容量 | 16 | 11 |
| 扩容方式 | 2 倍 | 2 倍 + 1 |
| 容量要求 | 2 的幂次方 | 任意 |
| 哈希计算 | 扰动函数 + 位运算 | hashCode + 取模 |
| 迭代器 | Iterator(fail-fast) | Iterator + Enumeration |
| 性能 | 高 | 较低 |
| 推荐使用 | ✅ 是(需同步时手动加锁) | ❌ 不推荐(遗留类) |
4. 并发场景下的替代方案
4.1 方案对比
| 方案 | 线程安全 | 性能 | 推荐场景 |
|---|---|---|---|
| Hashtable | ✅ | 差 | ❌ 不推荐 |
Collections.synchronizedMap |
✅ | 中 | 简单需求 |
| ConcurrentHashMap | ✅ | 高 | ✅ 强烈推荐 |
4.2 代码示例
// ❌ 不推荐:Hashtable(遗留,性能差)
Map<String, String> bad = new Hashtable<>();
// ⚠️ 可以但不够好:同步包装器
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// ✅ 最佳实践:ConcurrentHashMap(JDK1.5+)
Map<String, String> best = new ConcurrentHashMap<>();
// 单线程下直接用 HashMap
Map<String, String> single = new HashMap<>();
4.3 选型决策流程图
5. 常见面试追问
Q1:为什么 Hashtable 不允许 null?
A:历史遗留设计选择。JDK 1.0 的设计者认为 null 不是合法的键值。而 HashMap 的设计者认为 null 有意义(表示不存在)。
Q2:ConcurrentHashMap 如何做到比 Hashtable 性能更好?
A:
- Hashtable:整个表一把锁,任何操作都锁全表
- ConcurrentHashMap (JDK8):CAS + synchronized 锁桶(链表头),并发度大大提升
Q3:HashMap 在 JDK7 和 JDK8 中有什么区别?
| 版本 | 数据结构 | 扩容时 | 死循环问题 |
|---|---|---|---|
| JDK7 | 数组 + 链表 | 头插法 | ❌ 存在 |
| JDK8 | 数组 + 链表 + 红黑树 | 尾插法 | ✅ 解决 |
Q4:如何让 HashMap 线程安全?
三种方式:
// 1. 包装(推荐度:中)
Map<String, String> map1 = Collections.synchronizedMap(new HashMap<>());
// 2. 使用 ConcurrentHashMap(推荐度:高)
Map<String, String> map2 = new ConcurrentHashMap<>();
// 3. 使用 Hashtable(推荐度:低)
Map<String, String> map3 = new Hashtable<>();
6. 最佳实践建议
6.1 代码规范
// ✅ 单线程 - 直接用 HashMap
Map<String, User> userCache = new HashMap<>();
// ✅ 多线程 - ConcurrentHashMap
Map<String, Session> sessionMap = new ConcurrentHashMap<>();
// ❌ 不要再用 Hashtable(除非维护老项目)
Map<String, Object> legacy = new Hashtable<>(); // 不推荐
// ❌ 不要用 Collections.synchronizedMap 除非简单场景
// 因为它只是简单包装,迭代时仍需手动同步
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 迭代时必须同步
synchronized (syncMap) {
for (String key : syncMap.keySet()) {
// ...
}
}
6.2 一句话记忆
HashMap 线程不安全,性能高,允许 null;Hashtable 线程安全,性能低,不允许 null;高并发场景用 ConcurrentHashMap。
口诀:
HashMap 单兵强,有 null 且快;
Hashtable 老古董,全程锁慢慢来;
并发场景不用愁,ConcurrentHashMap 最实在。
7. 源码级对比速查表
| 特性 | HashMap (JDK8) | Hashtable |
|---|---|---|
| 底层结构 | 数组 + 链表 + 红黑树 | 数组 + 链表 |
| 树化阈值 | TREEIFY_THRESHOLD = 8 | 不支持 |
| 默认负载因子 | 0.75f | 0.75f |
| 最大容量 | 1 << 30 | Integer.MAX_VALUE |
| 获取索引 | (n - 1) & hash |
(hash & 0x7FFFFFFF) % tab.length |
如果你搞清楚了 HashMap 和 Hashtable 的区别,欢迎点赞、收藏、转发!有任何疑问,评论区一起交流~

|
🌺The End🌺点点关注,收藏不迷路🌺
|
更多推荐



所有评论(0)