Hashtable 类是早期的哈希表实现,与 HashMap 类似但属于遗留类,Hashtable 不允许使用 null 作为键或值,且方法是同步的(线程安全),但性能相对较低,通常推荐用 HashMap 替代。
Hashtable 类实现了一个哈希表,用于将键映射到值。任何非空对象都可用作键或值。注意,要成功地从哈希表中存储和检索对象,用作键的对象必须实现 hashCode() 方法和 equals() 方法。
Hashtable 实例有两个影响其性能的参数:初始容量和加载因子。
容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。请注意,哈希表是开放的:当发生 "哈希冲突" 时,单个桶会存储多个条目,这些条目必须按顺序搜索。
加载因子是衡量哈希表在自动扩容前允许达到的填充程度的指标(如达到容量的 75%,则进行扩容)。
通常,默认加载因子(0.75)在时间和空间成本之间提供了很好的平衡。较高的加载因子会减少空间开销,但会增加查找条目的时间成本(可以理解为加载因子越高,自动扩容条件越难触发,导致某个哈希表桶中冲突的键越多,存放条目的列表越长,搜索越耗时)。
初始容量控制着空间浪费与重新哈希操作需求之间的平衡,重新哈希操作非常耗时。如果初始容量大于 Hashtable 将要包含的最大条目数除以其加载因子,则永远不会发生重新哈希操作(例如,如果要存放 100 个条目,设置初始容量为 100 ÷ 0.75 ≈ 133.33,因此建议将初始容量设置为 134,134 × 0.75 = 100.5 ≥ 100 不会触发扩容)。但是,将初始容量设置得过高可能会浪费空间。
如果要向 Hashtable 中插入很多条目,创建一个具有足够大容量的哈希表可能会使条目插入更高效,而不是让它在需要时自动重新哈希来扩容。
下面的示例创建了一个数字哈希表,它使用数字的名称作为键:
Hashtable<String, Integer> numbers = new Hashtable<String, Integer>();
numbers.put("one", 1);
numbers.put("two", 2);
numbers.put("three", 3);要检索数字,请使用以下代码:
Integer n = numbers.get("two");
if (n != null) {
System.out.println("two = " + n);
}Hashtable 类所有“集合视图方法”返回的集合的 iterator() 方法返回的迭代器都是快速失败的:如果在迭代器创建之后的任何时间对 Hashtable 进行结构修改(除了通过迭代器自身的 remove 方法),迭代器将抛出 ConcurrentModificationException 异常。因此,在面对并发修改时,迭代器会快速失败,而不是在未来某个不确定的时间抛出任意、非确定性行为的风险。Hashtable 的 keys 和 elements 方法返回的枚举不是快速失败的。
请注意,迭代器的快速失败行为不能得到保证,因为通常来说,在存在非同步并发修改的情况下,不可能做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException 异常。因此,编写依赖于此异常来保证正确性的程序是错误的:迭代器的快速失败行为应仅用于检测错误。
从 Java 2 平台 v1.2 开始,此类被改造为实现 Map 接口,使其成为 Java 集合框架的成员。与新的集合实现不同,Hashtable 是同步的。如果不需要线程安全的实现,建议使用 HashMap 代替 Hashtable。如果需要线程安全的高并发实现,建议使用 ConcurrentHashMap 代替 Hashtable。
线程安全:Hashtable 是线程安全的,其所有方法都被 synchronized 修饰,因此在多线程环境下可以安全使用。但这也导致了性能开销较大,在单线程环境下通常推荐使用 HashMap。
存储结构:基于数组和链表实现(哈希表结构),数组中的每个元素是一个链表的头节点。通过 key 的哈希值确定元素在数组中的位置,当发生哈希冲突时,使用链表存储相同哈希值的元素。
键值约束:key 和 value 都不能为 null,否则会抛出 NullPointerException异常。这与 HashMap 不同,HashMap 允许 key 和 value 为 null。
初始容量:初始容量默认为 11。
加载因子:加载因子默认为 0.75,当元素数量达到容量 × 加载因子时,会进行扩容(容量变为原来的 2n + 1)。
Hashtable 实现了 Map 接口,提供了一系列操作键值对的方法:
put(K key, V value) 将指定的键值对存入哈希表,若键已存在则替换旧值。如果键已存在,返回被替换的旧值;如果为新键,返回 null。注意:key 或 value 为 null 时会抛出 NullPointerException 异常。
putAll(Map<? extends K, ? extends V> t) 将另一个 Map 中的所有键值对添加到当前哈希表中。注意:若传入的 Map 中包含 null 键或值,会抛出 NullPointerException 异常。
get(Object key) 根据指定的键获取对应的值。若键存在,返回对应的值;否则返回 null。注意:key 为 null 时会抛出 NullPointerException 异常;返回 null 可能表示键不存在,因为 Hashtable 不允许值为 null。
elements() 返回包含哈希表中所有值的枚举(Enumeration)。注意:枚举不是快速失败的(即遍历过程中修改哈希表不会抛出异常)。
keySet() 返回包含哈希表中所有键的 Set 集合。注意:返回的 Set 是哈希表的视图,修改 Set 会影响原哈希表;Set 的迭代器是快速失败的。
values() 返回包含哈希表中所有值的 Collection 集合。注意:返回的 Collection 是哈希表的视图,修改 Collection 会影响原哈希表;集合的迭代器是快速失败的。
remove(Object key) 根据指定的键删除对应的键值对。如果键存在,则返回被删除的键对应的旧值;若键不存在,返回 null。注意:key 为 null 时会抛出 NullPointerException 异常。
clear() 清空哈希表中所有的键值对。注意:清空后哈希表的 size 为 0,但容量不变。
containsKey(Object key) 判断哈希表中是否包含指定的键。若包含该键,返回 true;否则返回 false。注意:key 为 null 时会抛出 NullPointerException 异常。
containsValue(Object value) 判断哈希表中是否包含指定的值。若包含该值,返回 true;否则返回 false。注意:value 为 null 时会抛出 NullPointerException 异常;判断基于 equals 方法,性能较差(需遍历所有值)。
isEmpty() 判断哈希表是否为空(无任何键值对)。若为空,返回 true;否则返回 false。
size() 返回哈希表中键值对的数量,当前哈希表的条目数。
Hashtable 和 HashMap 的主要区别如下表:
特性 | Hashtable | HashMap |
线程安全 | 是(方法被 synchronized 修饰) | 否(需手动处理线程安全) |
键值是否允许为 null | 不允许 | 允许(key 可为 null 一次) |
继承父类 | Dictionary | AbstractMap |
初始容量 | 11 | 16 |
扩容机制 | 2n + 1 | 2n |
迭代器类型 | 枚举(Enumeration)和迭代器 | 迭代器(Iterator) |
下面是 Hashtable 的简单用法示例,演示常用方法的用法:
package com.hxstrive.java_collection.hashTable;
import java.util.Hashtable;
import java.util.Enumeration;
public class HashtableExample {
public static void main(String[] args) {
// 创建Hashtable实例
Hashtable<String, Integer> hashtable = new Hashtable<>();
// 添加元素
hashtable.put("Apple", 10);
hashtable.put("Banana", 20);
hashtable.put("Orange", 15);
// 获取元素
System.out.println("Apple count: " + hashtable.get("Apple"));
// 遍历key
System.out.println("Keys:");
for (String key : hashtable.keySet()) {
System.out.println(key);
}
// 遍历value(使用Enumeration)
System.out.println("Values:");
Enumeration<Integer> values = hashtable.elements();
while (values.hasMoreElements()) {
System.out.println(values.nextElement());
}
// 判断是否包含key
System.out.println("Contains 'Banana'? " + hashtable.containsKey("Banana"));
// 删除元素
hashtable.remove("Orange");
System.out.println("Size after removal: " + hashtable.size());
}
}运行结果:
Apple count: 10 Keys: Orange Apple Banana Values: 15 10 20 Contains 'Banana'? true Size after removal: 2
下面查看 Hashtable 类的源码,了解主要成员变量的定义,明确 Hashtable 底层是如何存储数据,源码如下:
/**
* @param <K> 键的类型,必须是对象类型(不能是基本数据类型)
* @param <V> 值的类型,必须是对象类型(不能是基本数据类型)
*/
public class Hashtable<K,V>
extends Dictionary<K,V> // 继承自抽象字典类,提供基础的键值对操作方法
implements Map<K,V>, // 实现 Map 接口,支持标准的映射表操作
Cloneable, // 支持克隆,可创建对象的浅拷贝
java.io.Serializable { // 支持序列化,可将对象转换为字节流进行传输或存储
/**
* 哈希表的底层数据结构,是一个 Entry 类型的数组。
* 每个数组元素代表一个哈希桶,桶中可能包含多个 Entry(通过链表连接解决哈希冲突)。
*/
private transient Entry<?,?>[] table;
/**
* 哈希表中存储的键值对总数。
* 注意:此值不等于 table 数组的长度(容量),而是实际存储的条目数量。
*/
private transient int count;
/**
* 哈希表的扩容阈值。当表中条目数量超过此值时,会触发 rehash(重哈希)操作,扩大表容量。
* 其值计算公式为:threshold = (int)(capacity * loadFactor)
*/
private int threshold;
/**
* 哈希表的负载因子,用于计算扩容阈值。
* 负载因子是实际条目数量与容量的比值,值越大表示哈希表越"满",哈希冲突概率越高。
* 默认值通常为 0.75,平衡空间利用率和查询效率。
*/
private float loadFactor;
/**
* 记录哈希表被结构性修改的次数。
* 结构性修改包括:添加/删除条目、重哈希(rehash)等改变内部结构的操作。
* 此值用于实现迭代器的"快速失败(fail-fast)"机制:当迭代过程中检测到结构性修改时,
* 会立即抛出 ConcurrentModificationException,避免迭代器使用不一致的数据。
*/
private transient int modCount = 0;
//...
}上述源码中,Entry 表示哈希表中的一个桶,那么 Entry 是如何定义的呢?Entry 是哈希表中的键值对实体类,实现了 Map.Entry 接口。每个 Entry 对象代表哈希表中的一个键值对,同时通过 next 字段形成链表,用于解决哈希冲突。注意,Entry 是一个静态嵌套类,不依赖于外部 Hashtable 类的实例。
源码如下:
/**
* @param <K> 键的类型
* @param <V> 值的类型
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
/**
* 键的哈希值,用于确定该 Entry 在哈希表中的存储位置。
* 预先计算并存储哈希值可以提高查询效率,避免每次操作时重复计算。
*/
final int hash;
/**
* 键对象,在哈希表中具有唯一性(不允许重复键)。
* 使用 final 修饰,确保键一旦设置就不能被修改,保证哈希表结构的稳定性。
*/
final K key;
/**
* 与键关联的值对象。
* 非 final 修饰,允许通过 setValue() 方法修改值。
*/
V value;
/**
* 指向下一个 Entry 的引用,用于处理哈希冲突。
* 当多个键计算出相同的哈希值时,这些 Entry 会通过此链表指针连接在一起,形成单链表。
*/
Entry<K,V> next;
/**
* 构造方法,创建一个 Entry 实例。
*
* @param hash 键的哈希值
* @param key 键对象
* @param value 与键关联的值对象
* @param next 下一个 Entry 节点(用于形成链表)
*/
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* 克隆当前 Entry 实例,实现浅拷贝。
* 注意:键和值对象本身不会被克隆,仅复制引用;链表结构会被递归克隆。
*
* @return 新的 Entry 实例,包含与当前实例相同的哈希值、键、值和链表结构
*/
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(
hash,
key,
value,
// 如果存在下一个节点,则递归克隆下一个节点;否则为 null
(next == null ? null : (Entry<K,V>) next.clone())
);
}
// 实现 Map.Entry 接口的方法
/**
* 获取当前 Entry 的键。
*
* @return 键对象
*/
public K getKey() {
return key;
}
/**
* 获取当前 Entry 的值。
*
* @return 与键关联的值对象
*/
public V getValue() {
return value;
}
/**
* 修改当前 Entry 的值,并返回旧值。
* Hashtable 不允许值为 null,因此会对 null 进行校验。
*
* @param value 新的值对象
* @return 被替换的旧值对象
* @throws NullPointerException 如果新值为 null
*/
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
/**
* 判断当前 Entry 与另一个对象是否相等。
* 两个 Entry 相等的条件是:键相等且值相等(均考虑 null 的情况)。
*
* @param o 要比较的对象
* @return 如果相等则返回 true,否则返回 false
*/
public boolean equals(Object o) {
// 检查 o 是否为 Map.Entry 类型
if (!(o instanceof Map.Entry<?, ?> e))
return false;
// 比较键是否相等(处理 null 情况)
// 比较值是否相等(处理 null 情况)
return (key == null ? e.getKey() == null : key.equals(e.getKey())) &&
(value == null ? e.getValue() == null : value.equals(e.getValue()));
}
/**
* 计算当前 Entry 的哈希值。
* 哈希值由键的哈希值与值的哈希值异或(XOR)得到,保证键或值变化时哈希值也会变化。
*
* @return Entry 的哈希值
*/
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
/**
* 返回 Entry 的字符串表示形式,格式为 "key=value"。
*
* @return 字符串表示
*/
public String toString() {
return key.toString() + "=" + value.toString();
}
}在 Hashtable 中,通过 rehash() 方法实现哈希表扩容,源码如下:
/**
* 增加哈希表的容量并重新组织内部结构,以更高效地容纳和访问条目。
* 当哈希表中的键数量超过其当前容量与负载因子的乘积(即阈值)时,此方法会被自动调用。
* 重哈希操作通过扩大容量并重新计算所有条目的存储位置,减少哈希冲突,提高操作效率。
*/
@SuppressWarnings("unchecked")
protected void rehash() {
// 记录当前哈希表的容量(底层数组长度)
int oldCapacity = table.length;
// 保存旧的哈希表数组引用
Entry<?,?>[] oldMap = table;
// 计算新容量:旧容量的2倍 + 1(使用位运算提高效率)
// 这种扩容策略既保证容量增长,又尽量避免新容量是2的幂(减少哈希冲突)
int newCapacity = (oldCapacity << 1) + 1;
// 处理容量溢出情况,确保不超过最大数组大小限制
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果旧容量已达到最大值,则不再扩容,直接返回
if (oldCapacity == MAX_ARRAY_SIZE)
return;
// 否则将新容量设为最大允许值
newCapacity = MAX_ARRAY_SIZE;
}
// 创建新的哈希表数组(新容量)
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
// 增加结构性修改计数器,用于迭代器的快速失败机制
modCount++;
// 计算并更新新的扩容阈值:新容量 * 负载因子(不超过最大数组大小+1)
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 将哈希表引用指向新数组
table = newMap;
// 将旧哈希表中的所有条目重新哈希到新表中
// 从旧数组的最后一个索引开始向前遍历
for (int i = oldCapacity ; i-- > 0 ;) {
// 遍历当前桶中的所有条目(处理链表结构)
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
// 保存当前条目引用
Entry<K,V> e = old;
// 移动到下一个条目(提前更新,避免后续操作影响遍历)
old = old.next;
// 计算当前条目在新表中的存储位置:
// 1. 哈希值与0x7FFFFFFF按位与,确保结果为正数(清除符号位)
// 2. 对新容量取模,得到新的索引位置
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 将当前条目插入到新表对应索引的链表头部(头插法)
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}上面对 Hashtable 最核心源码进行了分析。
更多信息参考 https://docs.oracle.com/javase/8/docs/api/java/util/Hashtable.html API 文档。