MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Hashtable的源码解读与学习心得

2022-12-237.2k 阅读

Java Hashtable 概述

在Java集合框架中,Hashtable 是一个古老的类,它实现了 Map 接口,用于存储键值对。Hashtable 最早出现在JDK 1.0版本中,它的设计初衷是提供一种高效的键值对存储方式,类似于哈希表的数据结构。

Hashtable 具有以下几个重要特点:

  1. 线程安全Hashtable 的所有操作都是线程安全的,这意味着多个线程可以同时访问 Hashtable 而不会出现数据不一致的问题。这是通过在方法上使用 synchronized 关键字实现的。
  2. 不允许null键和null值:与 HashMap 不同,Hashtable 不允许将 null 作为键或值。如果试图插入 null 键或值,将会抛出 NullPointerException
  3. 哈希算法Hashtable 使用哈希算法来确定键值对在内部数组中的存储位置,以实现快速的查找和插入操作。

Hashtable 源码结构分析

  1. 成员变量
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 类型的数组,用于存储键值对。EntryHashtable 内部定义的一个静态嵌套类,用于表示哈希表中的一个键值对。
  • count:表示哈希表中当前存储的键值对数量。
  • threshold:表示哈希表的阈值,当 count 达到 threshold 时,会进行扩容操作。
  • loadFactor:负载因子,用于衡量哈希表的拥挤程度。默认值为0.75f,当哈希表中的元素数量达到数组长度的 loadFactor 倍时,就会触发扩容。
  • modCount:记录哈希表结构发生变化的次数,用于实现快速失败机制(fail - fast mechanism),在迭代过程中如果发现 modCount 发生变化,会抛出 ConcurrentModificationException
  1. 构造函数
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。

核心方法源码解读

  1. 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 方法插入新的键值对。
  1. 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 对象,并将其插入到链表头部。
  1. 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 数组,容量为新容量。
  • 重新计算每个键值对在新数组中的索引位置,并将其插入到新数组中。
  1. 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() 方法获取相应的集合视图,这些集合视图都支持迭代操作。

  1. entrySet 方法
public Set<Map.Entry<K,V>> entrySet() {
    if (entrySet == null)
        entrySet = new EntrySet();
    return entrySet;
}

这里返回的 EntrySetHashtable 的一个内部类,实现了 Set<Map.Entry<K, V>> 接口。

  1. 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 方法调用 Hashtableclear 方法清空哈希表。

学习心得

通过对 Hashtable 源码的深入解读,我对Java集合框架中的哈希表实现有了更深刻的理解。以下是我的一些学习心得:

  1. 线程安全的实现 Hashtable 通过在方法上使用 synchronized 关键字来保证线程安全,这种方式虽然简单直接,但在高并发环境下可能会导致性能瓶颈,因为所有线程都需要竞争同一个锁。相比之下,ConcurrentHashMap 使用了更细粒度的锁机制,如分段锁(在早期版本中)或CAS操作(在JDK 8及以后),能够提供更好的并发性能。在实际应用中,如果不需要线程安全,HashMap 是一个更高效的选择;如果需要线程安全且对性能要求较高,ConcurrentHashMap 则更为合适。

  2. 哈希算法与冲突解决 Hashtable 使用简单的取模运算来确定键值对在数组中的存储位置,这种方式在哈希分布均匀的情况下表现良好,但如果哈希算法设计不当或数据分布不均匀,可能会导致大量的哈希冲突。Hashtable 通过链表法来解决冲突,即当多个键值对映射到同一个索引位置时,它们会以链表的形式存储。在JDK 8中,HashMap 对链表长度超过8时会将链表转换为红黑树,以提高查找性能,而 Hashtable 并没有这一优化。这启示我们在设计哈希表时,要充分考虑哈希算法的质量以及冲突解决策略对性能的影响。

  3. 不允许null键和null值 Hashtable 不允许 null 键和 null 值,这与 HashMap 形成鲜明对比。这种设计可能是出于线程安全和语义明确性的考虑。在多线程环境下,如果允许 null 值,可能会导致一些难以调试的空指针异常。而 HashMap 允许 null 键和 null 值,虽然增加了灵活性,但也需要在使用时更加小心。在实际编程中,我们需要根据具体的业务需求来选择合适的键值对存储结构。

  4. 迭代器与快速失败机制 Hashtable 的迭代器实现相对简单,通过将 Enumeration 适配成 Iterator 来实现迭代功能。同时,它利用 modCount 实现了快速失败机制,在迭代过程中如果哈希表结构发生变化(除了通过迭代器自身的 remove 方法),会抛出 ConcurrentModificationException。这一机制有助于在多线程环境下及时发现数据不一致问题,但也需要注意在使用迭代器时正确处理这种异常。

  5. 历史与兼容性 Hashtable 是Java集合框架中一个古老的类,它的设计思想和实现方式反映了早期Java的发展历程。虽然在现代Java编程中,HashMapConcurrentHashMap 已经成为更常用的选择,但了解 Hashtable 的源码对于深入理解Java集合框架的演进以及哈希表的基本原理仍然具有重要意义。同时,在一些需要与旧代码兼容的项目中,Hashtable 可能仍然会被使用。

总结与应用场景

  1. 应用场景 由于 Hashtable 的线程安全特性,它适用于一些对线程安全要求较高且操作频率不是特别高的场景,例如在一些传统的企业级应用中,可能会使用 Hashtable 来存储一些全局配置信息。但在大多数情况下,特别是在高并发环境下,ConcurrentHashMap 是更好的选择。

  2. 与其他集合类的比较HashMap 相比,Hashtable 线程安全但性能较低,不允许 null 键和 null 值;而 HashMap 非线程安全,允许 null 键和 null 值,性能较高。与 ConcurrentHashMap 相比,Hashtable 的线程安全实现方式较为简单粗暴,在高并发下性能不如 ConcurrentHashMap

  3. 优化建议 如果在代码中使用 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集合框架具有重要的参考价值。通过深入研究其源码,我们可以汲取其中的精华,并应用到实际的编程工作中。