Java Hashtable的源码解读与学习心得
Java Hashtable 概述
在Java集合框架中,Hashtable
是一个古老的类,它实现了 Map
接口,用于存储键值对。Hashtable
最早出现在JDK 1.0版本中,它的设计初衷是提供一种高效的键值对存储方式,类似于哈希表的数据结构。
Hashtable
具有以下几个重要特点:
- 线程安全:
Hashtable
的所有操作都是线程安全的,这意味着多个线程可以同时访问Hashtable
而不会出现数据不一致的问题。这是通过在方法上使用synchronized
关键字实现的。 - 不允许null键和null值:与
HashMap
不同,Hashtable
不允许将null
作为键或值。如果试图插入null
键或值,将会抛出NullPointerException
。 - 哈希算法:
Hashtable
使用哈希算法来确定键值对在内部数组中的存储位置,以实现快速的查找和插入操作。
Hashtable 源码结构分析
- 成员变量
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
private transient Entry<?,?>[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
}
table
:这是一个Entry
类型的数组,用于存储键值对。Entry
是Hashtable
内部定义的一个静态嵌套类,用于表示哈希表中的一个键值对。count
:表示哈希表中当前存储的键值对数量。threshold
:表示哈希表的阈值,当count
达到threshold
时,会进行扩容操作。loadFactor
:负载因子,用于衡量哈希表的拥挤程度。默认值为0.75f,当哈希表中的元素数量达到数组长度的loadFactor
倍时,就会触发扩容。modCount
:记录哈希表结构发生变化的次数,用于实现快速失败机制(fail - fast mechanism),在迭代过程中如果发现modCount
发生变化,会抛出ConcurrentModificationException
。
- 构造函数
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
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);
}
public Hashtable(Map<? extends K,? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
- 无参构造函数:使用默认的初始容量11和负载因子0.75f。
- 带初始容量的构造函数:可以指定初始容量,负载因子默认为0.75f。
- 带初始容量和负载因子的构造函数:允许用户自定义初始容量和负载因子。
- 带
Map
参数的构造函数:根据传入的Map
对象创建一个Hashtable
,初始容量为传入Map
大小的两倍(至少为11),负载因子为0.75f。
核心方法源码解读
- put 方法
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;
}
- 首先检查值是否为
null
,如果是则抛出NullPointerException
。 - 计算键的哈希值,并通过取模运算确定在数组中的索引位置。
- 从该索引位置开始遍历链表(如果存在冲突),查找是否有相同键的元素。如果找到,则更新值并返回旧值。
- 如果没有找到,则调用
addEntry
方法插入新的键值对。
- addEntry 方法
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;
}
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
- 增加
modCount
,表示结构发生变化。 - 检查当前元素数量是否达到阈值,如果达到则调用
rehash
方法进行扩容。 - 创建一个新的
Entry
对象,并将其插入到链表头部。
- rehash 方法
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
int newCapacity = oldCapacity * 2 + 1;
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;
}
}
}
- 计算新的容量,为旧容量的两倍加1。
- 创建一个新的
Entry
数组,容量为新容量。 - 重新计算每个键值对在新数组中的索引位置,并将其插入到新数组中。
- get 方法
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;
}
- 计算键的哈希值和索引位置。
- 从该索引位置开始遍历链表,查找与键匹配的元素,如果找到则返回其值,否则返回
null
。
迭代器相关源码分析
Hashtable
提供了 keys()
和 elements()
方法来获取键和值的枚举(Enumeration
),同时也支持通过 entrySet()
、keySet()
和 values()
方法获取相应的集合视图,这些集合视图都支持迭代操作。
- entrySet 方法
public Set<Map.Entry<K,V>> entrySet() {
if (entrySet == null)
entrySet = new EntrySet();
return entrySet;
}
这里返回的 EntrySet
是 Hashtable
的一个内部类,实现了 Set<Map.Entry<K, V>>
接口。
- EntrySet 类
private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return new EnumerationIterator<>(entryEnumerator());
}
public int size() {
return count;
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Entry<?,?> candidate = getEntry(key);
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return remove(key, value) != null;
}
public void clear() {
Hashtable.this.clear();
}
}
iterator
方法返回一个EnumerationIterator
,它是Hashtable
的另一个内部类,实现了Iterator
接口,将Enumeration
适配成Iterator
。size
方法返回哈希表中键值对的数量。contains
方法用于检查集合中是否包含指定的Map.Entry
。remove
方法用于从哈希表中移除指定的Map.Entry
。clear
方法调用Hashtable
的clear
方法清空哈希表。
学习心得
通过对 Hashtable
源码的深入解读,我对Java集合框架中的哈希表实现有了更深刻的理解。以下是我的一些学习心得:
-
线程安全的实现
Hashtable
通过在方法上使用synchronized
关键字来保证线程安全,这种方式虽然简单直接,但在高并发环境下可能会导致性能瓶颈,因为所有线程都需要竞争同一个锁。相比之下,ConcurrentHashMap
使用了更细粒度的锁机制,如分段锁(在早期版本中)或CAS操作(在JDK 8及以后),能够提供更好的并发性能。在实际应用中,如果不需要线程安全,HashMap
是一个更高效的选择;如果需要线程安全且对性能要求较高,ConcurrentHashMap
则更为合适。 -
哈希算法与冲突解决
Hashtable
使用简单的取模运算来确定键值对在数组中的存储位置,这种方式在哈希分布均匀的情况下表现良好,但如果哈希算法设计不当或数据分布不均匀,可能会导致大量的哈希冲突。Hashtable
通过链表法来解决冲突,即当多个键值对映射到同一个索引位置时,它们会以链表的形式存储。在JDK 8中,HashMap
对链表长度超过8时会将链表转换为红黑树,以提高查找性能,而Hashtable
并没有这一优化。这启示我们在设计哈希表时,要充分考虑哈希算法的质量以及冲突解决策略对性能的影响。 -
不允许null键和null值
Hashtable
不允许null
键和null
值,这与HashMap
形成鲜明对比。这种设计可能是出于线程安全和语义明确性的考虑。在多线程环境下,如果允许null
值,可能会导致一些难以调试的空指针异常。而HashMap
允许null
键和null
值,虽然增加了灵活性,但也需要在使用时更加小心。在实际编程中,我们需要根据具体的业务需求来选择合适的键值对存储结构。 -
迭代器与快速失败机制
Hashtable
的迭代器实现相对简单,通过将Enumeration
适配成Iterator
来实现迭代功能。同时,它利用modCount
实现了快速失败机制,在迭代过程中如果哈希表结构发生变化(除了通过迭代器自身的remove
方法),会抛出ConcurrentModificationException
。这一机制有助于在多线程环境下及时发现数据不一致问题,但也需要注意在使用迭代器时正确处理这种异常。 -
历史与兼容性
Hashtable
是Java集合框架中一个古老的类,它的设计思想和实现方式反映了早期Java的发展历程。虽然在现代Java编程中,HashMap
和ConcurrentHashMap
已经成为更常用的选择,但了解Hashtable
的源码对于深入理解Java集合框架的演进以及哈希表的基本原理仍然具有重要意义。同时,在一些需要与旧代码兼容的项目中,Hashtable
可能仍然会被使用。
总结与应用场景
-
应用场景 由于
Hashtable
的线程安全特性,它适用于一些对线程安全要求较高且操作频率不是特别高的场景,例如在一些传统的企业级应用中,可能会使用Hashtable
来存储一些全局配置信息。但在大多数情况下,特别是在高并发环境下,ConcurrentHashMap
是更好的选择。 -
与其他集合类的比较 与
HashMap
相比,Hashtable
线程安全但性能较低,不允许null
键和null
值;而HashMap
非线程安全,允许null
键和null
值,性能较高。与ConcurrentHashMap
相比,Hashtable
的线程安全实现方式较为简单粗暴,在高并发下性能不如ConcurrentHashMap
。 -
优化建议 如果在代码中使用
Hashtable
,并且发现性能瓶颈,可以考虑将其替换为ConcurrentHashMap
。如果不需要线程安全,HashMap
是更好的选择。另外,在使用Hashtable
时,要注意合理设置初始容量和负载因子,以减少扩容带来的性能开销。
通过对 Hashtable
源码的解读和分析,我们不仅掌握了哈希表的基本实现原理,还对Java集合框架的设计和实现有了更深入的认识。这将有助于我们在实际编程中选择合适的集合类,并优化代码性能。在今后的学习和工作中,我们应该不断深入研究Java集合框架的源码,以便更好地利用这些强大的工具。
代码示例
import java.util.Hashtable;
import java.util.Map;
public class HashtableExample {
public static void main(String[] args) {
// 创建一个Hashtable
Hashtable<String, Integer> hashtable = new Hashtable<>();
// 添加键值对
hashtable.put("one", 1);
hashtable.put("two", 2);
hashtable.put("three", 3);
// 获取值
Integer value = hashtable.get("two");
System.out.println("Value for key 'two': " + value);
// 遍历Hashtable
for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 删除键值对
hashtable.remove("three");
System.out.println("Hashtable size after removal: " + hashtable.size());
}
}
在上述代码中,我们创建了一个 Hashtable
,并进行了添加、获取、遍历和删除操作。通过这个简单的示例,可以直观地了解 Hashtable
的基本使用方法。同时,结合前面的源码分析,我们能更深入地理解这些操作在底层是如何实现的。
总之,Hashtable
作为Java集合框架中的一员,虽然在现代编程中使用场景相对较少,但它的设计和实现原理对于我们理解哈希表和Java集合框架具有重要的参考价值。通过深入研究其源码,我们可以汲取其中的精华,并应用到实际的编程工作中。