在 Java 中,HashMap 和 Hashtable 都是 Map 接口的实现类,用于存储键值对(key-value)数据(key 唯一、value 可重复),但二者在 线程安全、null 值支持、底层实现、性能 等核心维度差异显著。以下是详细解析:
一、核心概述
-
两者本质都是 “哈希表”,通过哈希函数将 key 映射到数组索引,解决哈希冲突(链表 / 红黑树)。
-
Hashtable是 JDK 1.0 推出的 遗留类,设计偏老旧;HashMap是 JDK 1.2 引入的 Collections Framework 核心类,专为高效性设计,是目前开发中最常用的 Map 实现。
二、核心区别(重点)
1. 线程安全与并发性能
| 特性 | Hashtable | HashMap |
|---|---|---|
| 线程安全 | 是(线程安全) | 否(非线程安全) |
| 实现方式 | 所有方法加 synchronized 锁(全局锁) | 无锁设计(默认非线程安全) |
| 并发性能 | 极低(多线程竞争同一把锁,阻塞严重) | 高(无锁 overhead,单线程效率极高) |
-
补充:若需线程安全的
HashMap,可使用Collections.synchronizedMap(new HashMap<>())(本质也是全局锁,效率和Hashtable接近),更推荐用ConcurrentHashMap(JDK 1.8+ 用 CAS + 分段锁,并发性能远超Hashtable)。
2. null 值支持
这是开发中最易踩坑的区别:
这是开发中最易踩坑的区别:
-
HashMap:允许 1 个 null key(key 唯一)和多个 null value。HashMap<String, String> map = new HashMap<>(); map.put(null, "a"); // 合法 map.put(null, "b"); // 覆盖前一个 null key 的值(最终 null → "b") map.put("c", null); // 合法 map.put(" d", null); // 合法(多个 null value)Hashtable:禁止 null key 和 null value,否则直接抛出NullPointerException。Hashtable<String, String> table = new Hashtable<>(); table.put(null, "a"); // 抛 NPE table.put("b", null); // 抛 NPE
3. 底层数据结构
-
HashMap(JDK 1.8+): 数组 + 链表 + 红黑树 (优化哈希冲突)-
数组(哈希桶)是核心,每个元素是链表的头节点;
-
当链表长度 > 8 且数组容量 ≥ 64 时,链表转为红黑树(查询时间复杂度从 O (n) 降为 O (log n));
-
当红黑树节点数 < 6 时,转回链表(节省空间)。
-
-
Hashtable: 始终是数组 + 链表 (无红黑树优化)-
哈希冲突时仅用链表解决,当数据量大时,链表过长会导致查询效率急剧下降(O (n))。
-
4. 初始容量与扩容机制
哈希表的 “容量” 是数组的长度,“负载因子”(默认 0.75)是扩容阈值的比例(容量 × 负载因子),当元素个数超过阈值时触发扩容。
| 特性 | Hashtable | HashMap |
|---|---|---|
| 初始容量 | 11 | 16(必须是 2 的幂) |
| 扩容规则 | 新容量 = 旧容量 × 2 + 1 | 新容量 = 旧容量 × 2(保持 2 的幂) |
| 负载因子 | 默认 0.75(可通过构造器修改) | 默认 0.75(可通过构造器修改) |
-
关键:
HashMap容量为 2 的幂,是为了用 位运算(&)替代取模(%) 计算哈希索引(index = hash & (capacity - 1)),效率更高(位运算比取模快);而Hashtable用取模计算索引(index = (hash & 0x7FFFFFFF) % capacity),效率较低。
5. 哈希计算方式
-
Hashtable:直接使用 key 的hashCode()作为哈希值,无二次处理:
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % capacity; // 取模确保索引非负
-
HashMap:对 key 的hashCode()进行 二次哈希 (hash()方法),减少哈希冲突:
static final int hash(Object key) {
int h;
// key 为 null 时 hash 为 0,否则二次哈希(高位参与运算)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int index = hash & (capacity - 1); // 位运算求索引
二次哈希的目的是让 key 的高位哈希值也参与索引计算,降低不同 key 映射到同一索引的概率。
6. 迭代器特性(fail-fast vs 非 fail-fast)
-
HashMap:迭代器(Iterator)是 fail-fast(快速失败) 的:-
迭代过程中,若通过非迭代器方法(如
put、remove)修改集合,会立即抛出ConcurrentModificationException(并发修改异常); -
原理:迭代器维护一个
modCount(修改次数),每次迭代时检查modCount是否变化。
-
-
Hashtable:枚举器(Enumerator)是 非 fail-fast 的:-
迭代过程中修改集合(如
put、remove)不会抛出异常,但迭代器本身不支持remove()方法(调用会抛UnsupportedOperationException); -
注意:
Hashtable也提供Iterator迭代器(兼容 Collections Framework),其Iterator同样是 fail-fast 的,仅原生Enumerator是非 fail-fast。
-
7. 继承关系与设计定位
-
Hashtable:继承自Dictionary类(已废弃的抽象类),同时实现Map接口,设计上存在历史包袱。 -
HashMap:直接实现Map接口,遵循 Collections Framework 规范,设计更简洁,功能更灵活(如支持forEach、compute等 JDK 8+ 新方法)。
三、完整对比表格
| 对比维度 | Hashtable | HashMap |
|---|---|---|
| 线程安全 | 是(synchronized 全局锁) | 否(默认非线程安全) |
| null 支持 | 禁止 key/value 为 null(抛 NPE) | 允许 1 个 null key + 多个 null value |
| 底层结构 | 数组 + 链表(无红黑树) | 数组 + 链表 + 红黑树(JDK 1.8+) |
| 初始容量 | 11 | 16(2 的幂) |
| 扩容规则 | 2n + 1 | 2n(保持 2 的幂) |
| 哈希计算 | 直接用 hashCode () + 取模 | 二次哈希(hash ()) + 位运算 |
| 迭代器 | Enumerator(非 fail-fast)、Iterator(fail-fast) | Iterator(fail-fast) |
| 效率 | 低(全局锁 + 链表查询慢) | 高(无锁 + 红黑树优化) |
| 设计定位 | JDK 1.0 遗留类 | JDK 1.2+ 主流 Map 实现 |
| 推荐场景 | 几乎不用(被 ConcurrentHashMap 替代) | 单线程 / 无并发场景(首选) |
四、使用场景与推荐实践
-
单线程环境(无并发修改):
-
优先用
HashMap(效率最高,支持 null 值,功能灵活); -
若需 “插入顺序 / 访问顺序”,用
LinkedHashMap(HashMap子类); -
若需排序(如按 key 自然排序),用
TreeMap。
-
-
多线程并发环境:
-
首选
ConcurrentHashMap(JDK 1.8+ 并发性能最优,支持高并发读写,无 null 键值限制); -
避免用
Hashtable或Collections.synchronizedMap(new HashMap<>())(二者都是全局锁,并发效率低)。
-
-
禁止 null 键值的场景:
-
不用
Hashtable,可通过业务逻辑校验 null,或用ConcurrentHashMap(手动避免 null 键值)。
-
-
历史项目兼容:
-
仅当维护老旧项目(JDK 1.0 遗留代码)时,才可能接触
Hashtable,新项目坚决不用。
-
五、关键总结
-
HashMap是单线程场景的首选,高效、灵活、功能完善; -
Hashtable是遗留类,线程安全但效率低、限制多(无 null 支持、无红黑树),已被ConcurrentHashMap替代; -
核心选择逻辑:单线程用 HashMap,多线程用 ConcurrentHashMap。

658

被折叠的 条评论
为什么被折叠?



