定义与基本原理
定义
Hashtable 是一个基于哈希表(Hash Table)实现的 Map 接口,它将键(Key)映射到值(Value)。它的核心特点是线程安全,不允许 null 键或 null 值。
核心原理
Hashtable 的底层数据结构是 数组 + 链表(即“拉链法”解决哈希冲突)。
- 哈希函数:通过 Key 的 hashCode() 计算哈希值,再通过取模运算(%)确定元素在数组中的索引位置。
- 冲突解决:当两个 Key 的哈希值映射到同一个数组索引时,Hashtable 会在该索引位置维护一个链表,将新元素追加到链表中。
- 扩容机制:当元素数量超过阈值(容量 × 负载因子)时,数组会自动扩容(通常是原容量的 2 倍 + 1),并重新分配所有元素的位置(Rehash)。
继承体系
Hashtable<K,V>是一种key-value结构,它继承自
Dictionary<K,V>,实现了Map<K,V>接口、Cloneable接口及Serializable接口。
源码剖析
类定义与成员变量
// 继承 Dictionary 类(这是一个过时的抽象类),实现了 Map, Cloneable, Serializable 接口 public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { // 哈希表的核心数组,存储 Entry 节点 protected transient Entry<?,?>[] table; // 元素总数 protected transient int count; // 扩容阈值 protected transient int threshold; // 负载因子 protected transient float loadFactor; // 修改次数,用于 Fail-Fast 机制 protected transient int modCount = 0; }构造函数
1.最核心的构造函数,负责实际的初始化工作
public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; table = new Entry<?,?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); }关键点
- 参数校验:确保初始容量和负载因子是有效的。
- 初始化数组:创建了
Entry数组,这是Hashtable存储数据的基础。 - 计算阈值:
threshold是判断Hashtable是否需要扩容的关键指标,其值为容量 * 负载因子。
2.无参构造函数
这是最常用的构造函数。如果不指定任何参数,Hashtable会使用默认的初始容量和负载因子。
public Hashtable() { this(11, 0.75f); }关键点
- 默认初始容量:
11。选择11这个质数是为了在使用取余法计算哈希索引时,能让数据分布得更均匀,减少哈希冲突。 - 默认负载因子:
0.75f。这是一个在时间和空间成本之间取得平衡的折中值。
3.指定初始容量的构造函数
当我们预估了Hashtable中大概会存放多少元素时,可以使用这个构造函数来避免频繁的扩容操作,提升性能。
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }关键点
- 它使用我们指定的
initialCapacity,但负载因子仍采用默认值0.75f。
4.根据已有 Map 初始化的构造函数
当我们需要用一个已经存在的Map来创建一个新的Hashtable时,这个构造函数非常方便。
public Hashtable(Map<? extends K, ? extends V> t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }关键点
- 智能容量:它会将新
Hashtable的初始容量设置为t.size()的两倍和11之间的较大值。这样做是为了给新集合留出足够的空间,避免在调用putAll(t)时立即触发扩容。 - 复制数据:初始化后,它会调用
putAll(t)将传入Map中的所有键值对复制到新的Hashtable中。
5.“包级私有”(package-private)的构造方法
这是一个为了解决Properties类(Hashtable的子类)在初始化时的一个特殊需求的构造方法。
Hashtable(Void dummy) {}关键点
- 访问权限:它没有
public修饰符。这意味着它只能在java.util包内部被访问。 - 参数:接收一个
Void类型的参数(通常传null),这只是一个占位符,用于区分构造函数签名。 - 逻辑:方法体是空的,什么都不做。
作用
这个构造函数的存在完全是为了服务于java.util.Properties类。
- 继承关系:
Properties类继承自Hashtable。 Properties的特殊性:Properties类主要用于处理配置文件(键值对都是字符串)。虽然它继承自Hashtable,但在 JDK 的实现中,Properties实际上维护了自己的底层存储结构(通常是ConcurrentHashMap),而不是使用父类Hashtable中的table数组。- 避免浪费:
当创建一个Properties对象时,如果调用Hashtable的默认构造函数,会初始化一个容量为 11 的Entry数组 (table = new Entry<?,?>[11])。但由于Properties根本不使用这个数组,这块内存分配就是纯粹的浪费。
Properties类在初始化时,会显式调用这个特殊的父类构造函数:
// 在 java.util.Properties 源码中 private Properties(Properties defaults, int initialCapacity) { // 调用 Hashtable 的空构造函数,跳过 Hashtable 的数组初始化 super((Void) null); // Properties 使用自己的 ConcurrentHashMap 作为底层存储 map = new ConcurrentHashMap<>(initialCapacity); this.defaults = defaults; }核心方法:put()
这是 Hashtable 最核心的写入操作,留心synchronized关键字的使用。
public synchronized V put(K key, V value) { // 1. 确保 value 不为 null(HashTable 不允许 null 值) if (value == null) { throw new NullPointerException(); } // 2. 计算 key 的哈希值 // 注意:这里直接使用 key.hashCode(),没有像 HashMap 那样做高位异或扰动 int hash = key.hashCode(); // 3. 计算数组索引 // 使用 & 0x7FFFFFFF 确保哈希值为正数,然后对数组长度取模 int index = (hash & 0x7FFFFFFF) % table.length; // 4. 遍历链表,检查 key 是否已存在 for (Entry<?,?> e = (Entry<?,?>)table[index] ; e != null ; e = e.next) { // 如果 key 相等(哈希相同且 equals 为 true),则更新旧值 if ((e.hash == hash) && e.key.equals(key)) { V old = (V)e.value; e.value = value; return old; } } // 5. 如果 key 不存在,执行插入操作(头插法) addEntry(hash, key, value, index); return null; }关键点
- 线程安全:方法上直接加了
synchronized,这意味着同一时间只有一个线程能执行put操作,这是 Hashtable 线程安全的根本原因,也是其性能瓶颈所在。 - 索引计算:使用取模运算
%,这在性能上略低于 HashMap 的位运算。
扩容机制:rehash()
当count >= threshold时触发。
protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // 1. 新容量 = 旧容量 * 2 + 1 // 注意:HashMap 是扩容为 2 的幂,而 HashTable 是 2n+1,目的是取质数减少冲突 int newCapacity = (oldCapacity << 1) + 1; Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; // 2. 重新计算阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; // 3. 重新分配元素 for (int i = 0 ; i < oldCapacity ; i++) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } }常用操作
Hashtable 的使用非常简单,符合标准 Map 的接口规范:
| 操作 | 方法 | 说明 |
|---|---|---|
| 添加/修改 | put(K key, V value) | 插入键值对,若 Key 存在则覆盖 Value。 |
| 获取 | get(Object key) | 根据 Key 获取 Value,找不到返回 null。 |
| 删除 | remove(Object key) | 删除指定 Key 的映射。 |
| 大小 | size() | 返回集合中键值对的数量。 |
| 判空 | isEmpty() | 判断集合是否为空。 |
| 遍历 | entrySet()/keySet() | 返回集合视图,用于迭代遍历。 |
Hashtable 与 HashMap 对比
| 特性 | HashTable | HashMap |
|---|---|---|
| 线程安全 | 是 (全表锁synchronized) | 否 (多线程下不安全) |
| Null 键/值 | 不允许 (抛出NullPointerException) | 允许 (1个 null 键,多个 null 值) |
| 底层结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 (JDK 1.8+) |
| 初始容量 | 11 | 16 |
| 扩容规则 | old * 2 + 1(趋向质数) | old << 1(2 的幂次方) |
| 哈希计算 | 直接使用hashCode() | 二次哈希(高位异或)+ 位运算 |
| 迭代器 | Enumeration(非 Fail-Fast) | Iterator(Fail-Fast) |
| 性能 | 较低 (锁竞争严重) | 较高 (无锁,单线程最优) |
详细解析:
数据结构差异(红黑树):
- HashMap在 JDK 1.8 引入了红黑树。当链表长度超过 8 且数组长度超过 64 时,链表会转为红黑树,查找时间复杂度从 O(n) 降为 O(logn) 。
- Hashtable始终只有链表,如果哈希冲突严重,性能会显著下降。
扩容与效率:
- HashMap的容量是 2 的幂(16, 32, 64...),这使得它在计算索引时可以使用位运算
(n-1) & hash代替取模运算,效率更高。 - Hashtable使用
2n+1扩容,且必须使用取模运算,计算开销稍大。
- HashMap的容量是 2 的幂(16, 32, 64...),这使得它在计算索引时可以使用位运算
Fail-Fast 机制:
- HashMap的迭代器是快速失败的。如果在遍历过程中结构被修改(非通过迭代器自身的 remove 方法),会立即抛出
ConcurrentModificationException。 - Hashtable的
Enumeration不是快速失败的,这在并发环境下可能导致不可预知的行为。
- HashMap的迭代器是快速失败的。如果在遍历过程中结构被修改(非通过迭代器自身的 remove 方法),会立即抛出
优缺点总结与替代方案
优点:
- 线程安全:在早期的 Java 版本中,它是多线程环境下唯一的选择。
- 简单:API 简单,无需额外配置即可在多线程中使用。
缺点:
- 性能低下:由于
synchronized锁住的是整个 Hashtable 对象,在高并发场景下,所有线程都在抢一把锁,导致吞吐量急剧下降。 - 功能陈旧:不支持红黑树优化,不支持 null 值,扩容机制也不如 HashMap 高效。
替代方案:
单线程环境:
- 首选
HashMap。它的性能最好,功能最全。
- 首选
多线程环境:
- 绝对不要使用
Hashtable。 - 首选
ConcurrentHashMap。- 在 JDK 1.7 中,它使用分段锁(Segment),锁粒度更细。
- 在 JDK 1.8 中,它放弃了 Segment,采用
CAS + synchronized锁住链表/红黑树的头节点,并发性能远高于 Hashtable。
- 备选
Collections.synchronizedMap(new HashMap<>())。这虽然也是全表锁,但比 Hashtable 稍微灵活一点(可以包装任何 Map),不过性能依然不如 ConcurrentHashMap。
- 绝对不要使用