Java Hashtable 完整教程
Hashtable 是 Java 集合框架中一个非常经典的类,它实现了 Map 接口,用于存储键值对,它的核心特点是线程安全和不允许键或值为 null。

目录
Hashtable是什么?- 为什么需要
Hashtable?(线程安全) Hashtable的核心特性- 如何使用
Hashtable?(基本操作)- 创建
Hashtable - 添加元素 (
put) - 获取元素 (
get) - 删除元素 (
remove) - 判断是否存在 (
containsKey,containsValue) - 遍历
Hashtable - 获取大小 (
size)
- 创建
Hashtable与HashMap的核心区别- 何时使用
Hashtable? - 现代替代方案:
ConcurrentHashMap - 完整代码示例
Hashtable 是什么?
Hashtable(哈希表)是基于哈希表数据结构的 Map 实现,它通过一个“键”来快速查找、插入和删除对应的“值”,内部,它使用一个数组来存储数据,并通过键的哈希码计算出在数组中的存储位置,从而实现高效的存取操作。
在 Java 中,Hashtable 继承自 Dictionary 类(一个已过时的类),并实现了 Map 接口。
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
// ... 内部实现
}
为什么需要 Hashtable?(线程安全)
Hashtable 最主要的特点是它是线程安全的,这意味着在多线程环境下,当多个线程同时读写同一个 Hashtable 实例时,不需要额外的同步措施(如 synchronized 关键字或 Lock 对象),Hashtable 内部的方法都是同步的,可以保证数据的一致性。
在 Java 的早期版本(JDK 1.2 之前)中,Hashtable 是主要的 Map 实现,并且是为多线程环境设计的。

Hashtable 的核心特性
- 线程安全:所有公共方法(如
get,put,remove等)都使用synchronized关键字进行同步,确保在多线程环境下的安全访问。 - 不允许
null键和null值:与HashMap不同,如果你尝试向Hashtable中插入一个null键或null值,它会抛出NullPointerException。 - 迭代器是 fail-fast 的:当
Hashtable被创建后,如果迭代器正在遍历,而另一个线程修改了Hashtable的结构(非迭代器自身的remove方法),迭代器会立即抛出ConcurrentModificationException。 - 不保证有序性:在 Java 1.8 之前,
Hashtable的迭代顺序是不确定的,在 Java 1.8 及之后,虽然底层实现有所优化,但它仍然不保证迭代的顺序(如插入顺序或访问顺序)。
如何使用 Hashtable?(基本操作)
创建 Hashtable
Hashtable 有几个构造函数,最常用的是指定初始容量和加载因子。
// 1. 创建一个默认的 Hashtable,初始容量为 11,加载因子为 0.75 Hashtable<String, Integer> scores = new Hashtable<>(); // 2. 创建一个指定初始容量的 Hashtable Hashtable<String, Integer> scoresWithCapacity = new Hashtable<>(20); // 3. 创建一个指定初始容量和加载因子的 Hashtable // 加载因子是 0.8,意味着当元素数量达到 容量 * 0.8 时,哈希表会进行扩容 Hashtable<String, Integer> scoresWithLoadFactor = new Hashtable<>(20, 0.8f);
添加元素 (put)
使用 put(K key, V value) 方法添加键值对,如果键已存在,则旧的值会被新的值覆盖。
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 76);
// 尝试添加 null 键或值会抛出 NullPointerException
// scores.put(null, 100); // 抛出 NullPointerException
// scores.put("David", null); // 抛出 NullPointerException
获取元素 (get)
使用 get(Object key) 方法根据键获取对应的值,如果键不存在,则返回 null。
Integer aliceScore = scores.get("Alice"); // 返回 95
Integer davidScore = scores.get("David"); // 返回 null
删除元素 (remove)
使用 remove(Object key) 方法删除指定的键值对,并返回被删除的值,如果键不存在,则返回 null。

Integer removedScore = scores.remove("Bob"); // 返回 88,并从表中移除 "Bob"
判断是否存在 (containsKey, containsValue)
containsKey(Object key):检查Hashtable中是否包含指定的键。containsValue(Object value):检查Hashtable中是否包含指定的值。
boolean hasAlice = scores.containsKey("Alice"); // 返回 true
boolean hasScore88 = scores.containsValue(88); // 返回 false (因为 Bob 已被删除)
遍历 Hashtable
有几种方式可以遍历 Hashtable:
使用 keySet() 和 for-each 循环(推荐)
System.out.println("遍历所有键:");
for (String name : scores.keySet()) {
System.out.println("Name: " + name + ", Score: " + scores.get(name));
}
使用 entrySet() 和 for-each 循环(最高效)
System.out.println("遍历所有键值对:");
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println("Name: " + entry.getKey() + ", Score: " + entry.getValue());
}
使用迭代器
System.out.println("使用迭代器遍历:");
Iterator<String> iterator = scores.keySet().iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println("Name: " + name + ", Score: " + scores.get(name));
}
获取大小 (size)
使用 size() 方法获取 Hashtable 中键值对的数量。
int size = scores.size(); // 返回 2 (Alice 和 Charlie)
Hashtable 与 HashMap 的核心区别
这是一个面试中非常常见的问题,以下是两者最主要的区别:
| 特性 | Hashtable |
HashMap |
|---|---|---|
| 线程安全 | 线程安全,方法使用 synchronized |
非线程安全 |
| Null 键和值 | 不允许 null 键和 null 值 |
允许一个 null 键和多个 null 值 |
| 性能 | 较慢,因为每个方法都有同步开销 | 更快,没有同步开销 |
| 父类 | 继承自过时的 Dictionary 类 |
实现了 Map 接口 |
| 迭代器 | 迭代器是 fail-fast 的 | 迭代器也是 fail-fast 的 |
| 遍历方式 | 使用 Enumeration 或迭代器 |
使用迭代器或 for-each 循环 |
| 推荐使用 | 在多线程环境中(但通常有更好的选择) | 在单线程环境中是首选 |
何时使用 Hashtable?
由于 Hashtable 的同步开销较大,在现代 Java 开发中,直接使用 Hashtable 的场景已经非常少。
- 不推荐使用
Hashtable的情况:在单线程应用中,应优先使用性能更高的HashMap,在多线程应用中,Hashtable也是一个可行的选择,但通常有更优的替代方案。
现代替代方案:ConcurrentHashMap
从 Java 5 开始,java.util.concurrent 包提供了 ConcurrentHashMap 类,它是 Hashtable 的一个现代、高性能替代品。
为什么 ConcurrentHashMap 更好?
- 更细粒度的锁:
Hashtable是在方法级别上锁的,意味着任何操作都会锁定整个表,而ConcurrentHashMap使用分段锁(在 Java 8 中改为CAS+synchronized),它只锁定需要修改的数据段,而不是整个表,这大大提高了并发性能。 - 更好的可伸缩性:在高并发场景下,
ConcurrentHashMap的吞吐量远高于Hashtable。
如果你需要一个线程安全的 Map,ConcurrentHashMap 几乎总是比 Hashtable 更好的选择。
完整代码示例
下面是一个综合示例,展示了 Hashtable 的基本用法和线程安全特性。
import java.util.Hashtable;
import java.util.Map;
public class HashtableExample {
public static void main(String[] args) {
// 1. 创建并初始化 Hashtable
Hashtable<String, String> capitalCities = new Hashtable<>();
capitalCities.put("USA", "Washington, D.C.");
capitalCities.put("UK", "London");
capitalCities.put("Germany", "Berlin");
capitalCities.put("France", "Paris");
System.out.println("初始的 Hashtable: " + capitalCities);
// 2. 获取元素
String capitalOfUSA = capitalCities.get("USA");
System.out.println("美国的首都是: " + capitalOfUSA);
// 3. 检查是否存在
boolean hasCanada = capitalCities.containsKey("Canada");
System.out.println("包含 'Canada' 吗? " + hasCanada);
// 4. 删除元素
capitalCities.remove("France");
System.out.println("删除 'France' 后: " + capitalCities);
// 5. 遍历 Hashtable
System.out.println("\n遍历所有国家及其首都:");
for (Map.Entry<String, String> entry : capitalCities.entrySet()) {
System.out.println("国家: " + entry.getKey() + ", 首都: " + entry.getValue());
}
// 6. 尝试添加 null 值(会抛出异常)
try {
capitalCities.put("Italy", null);
} catch (NullPointerException e) {
System.out.println("\n捕获到异常: " + e.getMessage());
}
// 7. 线程安全演示 (简单模拟)
// 注意:这个简单示例并不能完全展示线程安全,但我们可以相信 Hashtable 的设计
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
capitalCities.put("Thread-" + Thread.currentThread().getId() + "-" + i, "Value-" + i);
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\n多线程操作后的 Hashtable 大小: " + capitalCities.size());
}
}
Hashtable是一个线程安全的Map实现,适用于多线程环境。- 它的主要缺点是性能较低(因为方法级别的同步)和不允许
null键/值。 - 在现代 Java 编程中,对于单线程环境,应使用
HashMap。 - 对于多线程环境,应优先考虑使用
ConcurrentHashMap,因为它提供了更好的并发性能和可伸缩性。 Hashtable是 Java 历史发展的一部分,了解它有助于理解 Java 集合框架的演进,但在新项目中应谨慎选择。
