HashMap 与 Hashtable 的深度对比:从原理到实践

王者杯·14天创作挑战营·第3期 10w+人浏览 55人参与

在 Java 开发中,HashMap 和 Hashtable 是最常用的两种键值对存储结构,但很多时候我们可能只是默认使用HashMap,没有去深究两者的差异,对它们的区别并不完全清楚。今天我们就来深入剖析这两个类的差异,帮助我们在实际开发中做出正确选择。

一、历史渊源:从"古老"到"现代"的进化

1.1 诞生背景

  • Hashtable 是 Java 1.0 就存在的"元老级"类,属于早期集合框架的一部分,设计上带有上个世纪的编程风格。
  • HashMap 则是 Java 1.2 引入的新成员,属于现代 Java Collections Framework(JCF),吸收了多年的编程实践经验。

1.2 线程安全设计

这是两者最直观的区别:

  • Hashtable 的所有核心方法(如 putget)都被 synchronized 修饰,天生线程安全。
  • HashMap 则完全不考虑线程安全,这反而让它在单线程环境下性能更优。
// Hashtable 的线程安全实现(简化版)
public synchronized V put(K key, V value) {
    // 加锁逻辑...
}

// HashMap 的非线程安全实现(简化版)
public V put(K key, V value) {
    // 无锁逻辑...
}

为什么 Hashtable 的线程安全不被推荐?
因为它使用的是全局锁,所有线程都要竞争同一把锁,在高并发场景下会导致严重的性能瓶颈。

二、数据结构:从"简单"到"智能"的升级

2.1 底层实现对比

JDK 8 之后的 HashMap 采用了更先进的数据结构:

  • HashMap:数组 + 链表 + 红黑树
    当链表长度超过 8 且数组容量 ≥ 64 时,自动将链表转换为红黑树,把查询时间复杂度从 O(n) 优化到 O(log n)。

  • Hashtable:仅使用数组 + 链表
    无论链表多长都不会进化,极端情况下查询效率会大幅下降。

在这里插入图片描述

2.2 容量与扩容策略

  • HashMap

    • 初始容量默认 16(必须是 2 的幂)
    • 扩容时新容量 = 原容量 × 2(如 16 → 32 → 64)
    • 扩容因子 0.75(当元素数量超过容量×0.75时触发扩容)
  • Hashtable

    • 初始容量默认 11(不是 2 的幂)
    • 扩容时新容量 = 原容量 × 2 + 1(如 11 → 23 → 47)
    • 扩容因子同样是 0.75

为什么 HashMap 要用 2 的幂作为容量?
因为可以用位运算 (n-1) & hash 代替取模运算,大幅提升效率。而 Hashtable 由于容量不是 2 的幂,只能使用较慢的取模运算。

三、null 值处理:灵活与严格的选择

3.1 HashMap 的"宽容"设计

HashMap 对 null 值非常友好:

  • 允许一个 null 键(因为键必须唯一,多个 null 键会覆盖)
  • 允许任意数量的 null 值
  • 常用于需要处理缺失值的场景
HashMap<String, String> map = new HashMap<>();
map.put(null, "nullKeyValue");   // 合法
map.put("normalKey", null);      // 合法

3.2 Hashtable 的"严格"限制

Hashtable 对 null 值非常严格:

  • 不允许 null 键
  • 不允许 null 值
  • 任何尝试存储 null 的操作都会抛出 NullPointerException
Hashtable<String, String> table = new Hashtable<>();
table.put(null, "error");  // 抛出 NullPointerException
table.put("key", null);    // 抛出 NullPointerException

设计理念差异
HashMap 认为 null 是一种合法的"不存在"状态,而 Hashtable 认为所有数据都必须是明确存在的。

四、性能对比:速度与安全的权衡

4.1 单线程场景

在单线程环境下,HashMap 的性能明显优于 Hashtable:

  • 无锁开销,所有操作更轻量
  • 更优的哈希算法和数据结构
  • 测试数据:HashMap 的 put 操作比 Hashtable 快约 30%

4.2 多线程场景

Hashtable 虽然线程安全,但性能极差:

  • 全局锁导致所有操作串行化
  • 高并发下吞吐量可能下降 10 倍以上
  • 正确做法:使用 ConcurrentHashMap 替代
单线程性能
操作类型
HashMap: 1000 ops/ms
Hashtable: 700 ops/ms
多线程性能
并发数=100
HashMap: 并发异常
Hashtable: 50 ops/ms
ConcurrentHashMap: 800 ops/ms

五、迭代器与遍历方式

5.1 HashMap 的迭代器

  • 使用标准的 Iterator
  • 支持 fail-fast 机制:当集合被并发修改时,会抛出 ConcurrentModificationException
  • 推荐遍历方式:
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

5.2 Hashtable 的双重遍历方式

  • 同时支持 Iterator 和古老的 Enumeration
  • Iterator 支持 fail-fast,Enumeration 不支持
  • 古老的遍历方式:
Enumeration<String> keys = hashtable.keys();
while (keys.hasMoreElements()) {
    String key = keys.nextElement();
    System.out.println(key + ": " + hashtable.get(key));
}

六、实际应用场景建议

6.1 优先选择 HashMap

  • 场景:单线程环境、需要处理 null 值、追求高性能
  • 示例:缓存系统、本地数据结构、单例服务中的数据存储

6.2 谨慎使用 Hashtable

  • 场景:仅在以下情况考虑
    • 需要兼容 JDK 1.0 时代的遗留代码
    • 简单场景下的线程安全(但更推荐 Collections.synchronizedMap(hashMap)

6.3 多线程场景的正确选择

  • 高并发场景一律使用 ConcurrentHashMap
  • 它采用分段锁(JDK 7)或 CAS + 红黑树(JDK 8)实现,性能远超 Hashtable

七、总结:一张表看懂所有区别

特性HashMapHashtable
线程安全非线程安全线程安全(全局锁)
null 支持允许 null 键和 null 值不允许 null 键/值
数据结构数组+链表+红黑树(JDK 8+)数组+链表
初始容量1611
扩容机制容量 × 2容量 × 2 + 1
迭代器Iterator(支持 fail-fast)Iterator 和 Enumeration
推荐场景单线程环境、高性能需求遗留代码兼容、简单线程安全

核心思想:HashMap 代表了"现代 Java"的设计哲学——牺牲默认的线程安全,换取更高的性能和灵活性;而 Hashtable 则是"古老 Java"的产物,虽然线程安全但设计不够精巧。在实际开发中,除了极少数兼容场景,HashMap 都是更优的选择。

如果你需要线程安全的哈希表,记住:最好不要使用 Hashtable,而是使用 ConcurrentHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值