Java 集合:Hashtable 类

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 主要特性

  • 线程安全:Hashtable 是线程安全的,其所有方法都被 synchronized 修饰,因此在多线程环境下可以安全使用。但这也导致了性能开销较大,在单线程环境下通常推荐使用 HashMap。

  • 存储结构:基于数组和链表实现(哈希表结构),数组中的每个元素是一个链表的头节点。通过 key 的哈希值确定元素在数组中的位置,当发生哈希冲突时,使用链表存储相同哈希值的元素。

  • 键值约束:key 和 value 都不能为 null,否则会抛出 NullPointerException异常。这与 HashMap 不同,HashMap 允许 key 和 value 为 null。

  • 初始容量:初始容量默认为 11。

  • 加载因子:加载因子默认为 0.75,当元素数量达到容量 × 加载因子时,会进行扩容(容量变为原来的 2n + 1)。

   

Hashtable 常用方法

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()  返回哈希表中键值对的数量,当前哈希表的条目数。

  

与 HashMap 的主要区别

Hashtable 和 HashMap 的主要区别如下表:

特性

Hashtable

HashMap

线程安全

是(方法被 synchronized 修饰)

否(需手动处理线程安全)

键值是否允许为 null

不允许

允许(key 可为 null 一次)

继承父类

Dictionary

AbstractMap

初始容量

11

16

扩容机制

2n + 1

2n

迭代器类型

枚举(Enumeration)和迭代器

迭代器(Iterator)

  

Hashtable 简单用法

下面是 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 类的源码,了解主要成员变量的定义,明确 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 文档。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号