一、💡 一句话理解
核心结论
HashMap和Hashtable都用哈希表保存键值对,但HashMap非线程安全、允许null,现代实现还会把过长链表转成红黑树;Hashtable的主要方法使用synchronized、不允许null,属于较老的实现。
Java 中正确的类名是 Hashtable,不是 HashTable。
二、🧭 理论:它们是什么
2.1 先认识 Hashtable
Hashtable<K, V> 是 Java 早期提供的 Map 实现,用 Key 查找 Value,例如:
Map<String, Integer> scores = new Hashtable<>();
scores.put("语文", 90);
scores.put("数学", 95);
// get:根据 Key 取出对应的 Value。
System.out.println(scores.get("语文")); // 90它和 HashMap 的基本用途相同,都适合表达“姓名 → 电话”“商品编号 → 库存”这样的对应关系。最大的特点是:主要方法自带同步控制,因此常被理解为“加了大锁的老版 HashMap”。
2.2 Hashtable 和哈希表不是同一个概念
- **哈希表(hash table)**是一种数据结构;
Hashtable是 Java 中一个具体的类;HashMap虽然类名不是Hashtable,底层同样使用了哈希表。
因此,不能认为只有 Hashtable 才是哈希表。
2.3 为什么现在很少使用
Hashtable 出现得很早,后来才被纳入 Java Collections Framework。现代代码通常这样选择:
| 场景 | 常见选择 |
|---|---|
| 不需要并发访问 | HashMap |
| 多线程共享并修改 | ConcurrentHashMap |
| 维护历史代码或调用老式 API | 可能遇到 Hashtable |
你也可能间接使用过它:Properties 继承自 Hashtable<Object, Object>。
可以把 Map 想成一排储物柜:
hashCode():根据 Key 算出大致放进哪个柜子;- 桶(bucket):真正存放键值对的位置;
equals():同一个柜子里出现多个 Key 时,再确认是不是正在寻找的 Key。
类比的边界是:真实实现不是直接拿 hashCode() 当数组下标,还会经过扰动和取模或位运算。
2.4 主要区别
| 对比项 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 不保证线程安全 | 主要公开方法使用 synchronized |
null | 允许一个 null Key、多个 null Value | Key 和 Value 都不能为 null |
| 底层结构 | 数组 + 链表 + 红黑树(JDK 8+) | 数组 + 链表 |
| 默认容量 | 16 | 11 |
| 默认负载因子 | 0.75 | 0.75 |
| 扩容方式 | 通常扩大为原来的 2 倍 | 通常扩大为原来的 2 倍再加 1 |
| 推荐场景 | 普通 Map 的常用选择 | 兼容旧代码,现代业务很少新用 |
线程安全不等于性能更好
Hashtable通常给整个实例加锁,并发访问容易互相等待。需要高并发共享 Map 时,一般使用ConcurrentHashMap。
三、⚙️ 理论:底层怎么工作
3.1 HashMap 的 put 流程
- 根据 Key 计算哈希值和数组下标。
- 桶为空,直接放入新节点。
- 桶不为空,通过
hash和equals()判断 Key 是否已经存在。 - Key 已存在就覆盖 Value;否则把节点加入链表或红黑树。
- 元素数量超过阈值
容量 × 负载因子时扩容。
JDK 8+ 中,一个桶里的节点达到树化条件时会转成红黑树。常见面试数值是:链表节点达到 8,并且数组容量至少为 64;容量不足 64 时优先扩容,而不是立即树化。
数组 table
├─ 桶 0 → null
├─ 桶 1 → Node → Node 链表
└─ 桶 2 → TreeNode ↔ TreeNode 红黑树3.2 Hashtable 的 put 流程
Hashtable 同样先计算哈希值和桶下标,再用链表解决哈希冲突。主要区别是:
put、get等主要方法带有synchronized,同一时刻通常只有一个线程能执行实例上的这些方法;- 不支持
nullKey 和nullValue; - 冲突增多时仍使用链表,不会像现代
HashMap那样树化。
可以把同步过程理解成:
线程 A:拿到同一个 Hashtable 的锁 → 执行 put → 释放锁
线程 B: 等待拿锁 → 执行 get → 释放锁源码形式可以简化理解为:
public synchronized V put(K key, V value) {
// synchronized:执行该方法前,线程要先获得当前 Hashtable 实例的锁。
// 其他线程暂时不能同时执行这个实例上同样需要该锁的同步代码。
}不过,单个方法同步不代表组合操作一定安全:
// containsKey 和 put 分别是同步方法,
// 但其他线程仍可能在两次方法调用之间插入操作。
if (!hashtable.containsKey("count")) {
hashtable.put("count", 1);
}如果需要原子地完成“判断后修改”,应使用合适的并发 API,或对整个组合操作额外同步。
共同点
两者都依赖 Key 正确实现
hashCode()和equals()。如果只重写其中一个,可能出现“看起来相同的 Key,却取不到 Value”的问题。
四、🚀 实践:从准备到验证
4.1 前置准备
- JDK 8 或更高版本;
- 不需要第三方依赖。
4.2 可以拿来干什么
- 单线程、方法内临时数据或不共享的 Map:使用
HashMap。 - 多线程只读:正确构建并安全发布后,可以读取
HashMap。 - 多线程共享且会修改:优先考虑
ConcurrentHashMap,不要因为Hashtable名字里有 Table 就默认选择它。
4.3 完整实践:代码、启动和验证
新建 MapDifferenceDemo.java:
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class MapDifferenceDemo {
public static void main(String[] args) {
// HashMap 允许 null Key 和 null Value。
Map<String, String> hashMap = new HashMap<>();
hashMap.put(null, "空键");
hashMap.put("name", null);
System.out.println("HashMap:" + hashMap);
// Hashtable 的主要方法有同步控制,但不允许 null。
Map<String, String> hashtable = new Hashtable<>();
hashtable.put("name", "Afyke");
System.out.println("Hashtable:" + hashtable);
try {
// 这里会抛出 NullPointerException。
hashtable.put(null, "空键");
} catch (NullPointerException exception) {
System.out.println("Hashtable 不允许 null Key");
}
}
}运行:
javac MapDifferenceDemo.java
java MapDifferenceDemo预期能看到 HashMap 保存了 null,而 Hashtable 在写入 null Key 时抛出 NullPointerException。Map 的打印顺序不固定,不影响结论。
五、🎤 面试时怎么流利地说
5.1 回答路线
共同点 → 线程安全与 null → 底层结构 → 使用选择。
5.2 30 秒标准回答
HashMap和Hashtable都基于哈希表实现 Map。主要区别是HashMap非线程安全,允许一个nullKey 和多个nullValue;Hashtable的主要方法使用synchronized,Key 和 Value 都不能为null。底层上,JDK 8 之后的HashMap是数组、链表和红黑树,冲突严重时可以树化;Hashtable主要还是数组加链表。实际开发中,普通场景用HashMap,并发场景通常用ConcurrentHashMap,很少新用Hashtable。
5.3 1—2 分钟展开回答
两者放入元素时,都会根据 Key 的 hashCode() 定位桶,再通过 equals() 判断 Key 是否相同。发生哈希冲突时,HashMap 先使用链表;JDK 8+ 满足条件后会转成红黑树,降低极端冲突下的查询成本。
Hashtable 通过方法级同步保证单次方法调用的线程安全,但锁的粒度较大,而且“先判断再修改”这类组合操作仍需要额外保证原子性。因此,不能只因为它线程安全就优先使用它。
5.4 高频追问与短答
5.4.1 为什么 HashMap 允许 null?
HashMap 对 null Key 做了专门处理,可以把它放到特定桶中。Hashtable 会直接拒绝 null Key 和 Value。
5.4.2 Hashtable 完全可以代替 ConcurrentHashMap 吗?
不建议。两者都能提供线程安全的 Map 操作,但 Hashtable 的锁粒度较大;ConcurrentHashMap 专门面向高并发设计,通常有更好的并发性能。
5.4.3 Hashtable 单个方法同步,组合操作也一定安全吗?
不一定。例如“先 containsKey,再 put”包含两次调用,其他线程可能在中间插入操作。需要原子组合操作时,应选择合适的并发 API 或额外同步。
六、📌 总结
HashMap:非线程安全、允许null、JDK 8+ 支持链表树化。Hashtable:主要方法同步、不允许null、数组加链表。- 两者都依赖正确的
hashCode()和equals()。 - 普通场景用
HashMap;并发修改优先考虑ConcurrentHashMap。
一句话记忆
HashMap 灵活常用但不自带锁;Hashtable 自带大锁但已经偏老;并发优先 ConcurrentHashMap。
相关笔记:1.hashmap存储在方法内是线程安全的吗、../基础/1.HashMap、../基础/2.ConcurrentHashMap