一、引言
在 Java 的集合框架里,Hashtable
是一个历史悠久的数据结构,它提供了存储键值对的功能,并且能依据键高效地查找对应的值。虽然在现代 Java 开发中,HashMap
更为常用,但 Hashtable
也有其独特的优势,比如它是线程安全的。本文将深入剖析 Hashtable
的原理,包含底层数据结构、核心属性、构造方法、常用操作的实现细节等,同时结合代码示例辅助理解。
二、Hashtable 概述
2.1 定义与用途
Hashtable
位于 java.util
包下,是一个实现了 Map
接口的类。它主要用于存储键值对,其中键和值都不能为 null
。Hashtable
的主要用途是在多线程环境下,提供线程安全的键值对存储和查找功能。
2.2 继承关系与实现接口
Hashtable
的继承关系如下:
plaintext
java.lang.Object
└─ java.util.Dictionary<K,V>
└─ java.util.Hashtable<K,V>
它实现了 Map<K,V>
、Cloneable
和 java.io.Serializable
接口,具备克隆和序列化的能力。
java
import java.util.Hashtable;
import java.util.Map;
public class HashtableOverview {
public static void main(String[] args) {
// 创建一个 Hashtable 对象
Hashtable<String, Integer> hashtable = new Hashtable<>();
// 可以将其赋值给 Map 接口类型的变量
Map<String, Integer> map = hashtable;
}
}
三、底层数据结构:哈希表(数组 + 链表)
3.1 哈希表的基本概念
哈希表是一种根据键(Key)直接访问内存存储位置的数据结构。它借助哈希函数把键映射到一个固定大小的数组中的某个位置,这个位置被称作桶(Bucket)。当多个键映射到同一个桶时,就会发生哈希冲突。
3.2 Hashtable 的实现方式
Hashtable
的底层数据结构是数组 + 链表。数组中的每个元素是一个链表的头节点,当发生哈希冲突时,新的键值对会以链表的形式添加到对应桶的链表中。查找元素时,先通过哈希函数找到对应的桶,然后遍历链表找到目标键值对。
3.3 节点结构
Hashtable
中的节点是 Entry
类型,其定义如下:
java
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
每个 Entry
节点包含键、值、哈希值和指向下一个节点的引用。
四、核心属性
Hashtable
有几个重要的核心属性,这些属性控制着 Hashtable
的行为和性能:
java
// 存储数据的数组
private transient Entry<?,?>[] table;
// 键值对的数量
private transient int count;
// 扩容阈值,当 count 达到 threshold 时,会进行扩容
private int threshold;
// 负载因子,默认为 0.75
private float loadFactor;
// 修改次数,用于快速失败机制
private transient int modCount = 0;
table
:存储键值对的数组,数组的每个元素是一个链表的头节点。count
:表示Hashtable
中键值对的数量。threshold
:扩容阈值,计算公式为threshold = capacity * loadFactor
,当count
超过threshold
时,会进行扩容操作。loadFactor
:负载因子,默认值为 0.75,它决定了哈希表在多满时进行扩容。modCount
:记录Hashtable
的修改次数,用于快速失败机制,当在迭代过程中检测到modCount
发生变化时,会抛出ConcurrentModificationException
。
五、构造方法
5.1 无参构造方法
java
public Hashtable() {
this(11, 0.75f);
}
无参构造方法会调用另一个构造方法,初始容量为 11,负载因子为 0.75。
5.2 指定初始容量的构造方法
java
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
该构造方法允许指定 Hashtable
的初始容量,负载因子使用默认值 0.75。
5.3 指定初始容量和负载因子的构造方法
java
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);
}
此构造方法允许同时指定初始容量和负载因子。如果初始容量为 0,则将其设置为 1。
5.4 从其他 Map
创建 Hashtable
的构造方法
java
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
该构造方法接受一个 Map
对象作为参数,将该 Map
中的所有键值对添加到新创建的 Hashtable
中。初始容量为 Math.max(2*t.size(), 11)
,负载因子为 0.75。
java
import java.util.Hashtable;
import java.util.Map;
import java.util.HashMap;
public class HashtableConstructors {
public static void main(String[] args) {
// 无参构造方法
Hashtable<String, Integer> hashtable1 = new Hashtable<>();
// 指定初始容量的构造方法
Hashtable<String, Integer> hashtable2 = new Hashtable<>(20);
// 指定初始容量和负载因子的构造方法
Hashtable<String, Integer> hashtable3 = new Hashtable<>(15, 0.8f);
// 从其他 Map 创建 Hashtable 的构造方法
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("apple", 1);
hashMap.put("banana", 2);
Hashtable<String, Integer> hashtable4 = new Hashtable<>(hashMap);
System.out.println("Hashtable1: " + hashtable1);
System.out.println("Hashtable2: " + hashtable2);
System.out.println("Hashtable3: " + hashtable3);
System.out.println("Hashtable4: " + hashtable4);
}
}
六、常用操作原理
6.1 插入元素(put 方法)
java
public synchronized V put(K key, V value) {
// 确保值不为 null
if (value == null) {
throw new NullPointerException();
}
// 确保键不在表中
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// 扩容操作
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建新的 Entry 并插入到链表头部
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
put
方法的主要步骤如下:
- 检查值是否为
null
,如果为null
则抛出NullPointerException
。 - 计算键的哈希值和桶的索引。
- 遍历链表,检查键是否已经存在,如果存在则更新值并返回旧值。
- 如果键不存在,调用
addEntry
方法添加新的键值对。 - 在
addEntry
方法中,检查是否需要扩容,如果需要则调用rehash
方法进行扩容。 - 创建新的
Entry
节点,并将其插入到链表的头部。
6.2 获取元素(get 方法)
java
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
get
方法的主要步骤如下:
- 计算键的哈希值和桶的索引。
- 遍历链表,检查键是否存在,如果存在则返回对应的值,否则返回
null
。
6.3 删除元素(remove 方法)
java
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
remove
方法的主要步骤如下:
- 计算键的哈希值和桶的索引。
- 遍历链表,找到要删除的键值对。
- 如果找到,将该节点从链表中移除,并更新
modCount
和count
。 - 返回被删除的值,如果未找到则返回
null
。
6.4 扩容操作(rehash 方法)
java
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 新容量为旧容量的 2 倍 + 1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
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;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
rehash
方法的主要步骤如下:
- 计算新的容量,新容量为旧容量的 2 倍 + 1。
- 创建一个新的数组,大小为新容量。
- 遍历旧数组,将每个链表中的节点重新计算哈希值和索引,插入到新数组中。
- 更新
threshold
和table
。
java
import java.util.Hashtable;
public class HashtableOperations {
public static void main(String[] args) {
Hashtable<String, Integer> hashtable = new Hashtable<>();
// 插入元素
hashtable.put("apple", 1);
hashtable.put("banana", 2);
hashtable.put("cherry", 3);
// 获取元素
Integer value = hashtable.get("banana");
System.out.println("Value of banana: " + value);
// 删除元素
hashtable.remove("cherry");
System.out.println("After removing cherry: " + hashtable);
}
}
七、线程安全机制
Hashtable
是线程安全的,它通过在方法上使用 synchronized
关键字来实现。这意味着在同一时间只能有一个线程访问 Hashtable
的方法,从而保证了数据的一致性。但这种方式也带来了一定的性能开销,因为在多线程环境下,线程之间需要竞争锁。
八、性能分析
8.1 时间复杂度
- 插入操作:平均情况下,插入操作的时间复杂度为 O(1)。因为哈希表可以通过哈希函数快速定位到桶的位置,在没有哈希冲突的情况下,插入操作可以在常数时间内完成。但在极端情况下,当所有元素都映射到同一个桶时,插入操作的时间复杂度会退化为 O(n)。
- 查找操作:平均情况下,查找操作的时间复杂度为 O(1)。同样,哈希表可以通过哈希函数快速定位到桶的位置,然后在桶中查找元素。
- 删除操作:平均情况下,删除操作的时间复杂度为 O(1)。通过哈希函数定位到桶的位置,然后在桶中删除元素。
8.2 空间复杂度
Hashtable
的空间复杂度为 O(n),主要用于存储数组和链表节点。
九、注意事项
9.1 键和值不能为 null
Hashtable
不允许键和值为 null
,如果尝试插入 null
键或 null
值,会抛出 NullPointerException
。
9.2 线程安全与性能
虽然 Hashtable
是线程安全的,但由于使用了 synchronized
关键字,在高并发场景下可能会导致性能瓶颈。如果不需要线程安全,可以考虑使用 HashMap
;如果需要线程安全且对性能有较高要求,可以考虑使用 ConcurrentHashMap
。
十、总结
Hashtable
是 Java 中一个经典的键值对存储数据结构,它通过哈希表(数组 + 链表)实现了高效的键值对存储和查找。通过 synchronized
关键字保证了线程安全,但也带来了一定的性能开销。在使用 Hashtable
时,需要注意键和值不能为 null
,以及根据实际场景选择合适的并发方案。深入理解 Hashtable
的原理和性能特点,有助于我们在实际开发中更好地利用它来解决各种问题。