深度解析Hashtable工作原理

一、引言

在 Java 的集合框架里,Hashtable 是一个历史悠久的数据结构,它提供了存储键值对的功能,并且能依据键高效地查找对应的值。虽然在现代 Java 开发中,HashMap 更为常用,但 Hashtable 也有其独特的优势,比如它是线程安全的。本文将深入剖析 Hashtable 的原理,包含底层数据结构、核心属性、构造方法、常用操作的实现细节等,同时结合代码示例辅助理解。

二、Hashtable 概述

2.1 定义与用途

Hashtable 位于 java.util 包下,是一个实现了 Map 接口的类。它主要用于存储键值对,其中键和值都不能为 nullHashtable 的主要用途是在多线程环境下,提供线程安全的键值对存储和查找功能。

2.2 继承关系与实现接口

Hashtable 的继承关系如下:

plaintext 复制代码
java.lang.Object
    └─ java.util.Dictionary<K,V>
        └─ java.util.Hashtable<K,V>

它实现了 Map<K,V>Cloneablejava.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 方法的主要步骤如下:

  1. 检查值是否为 null,如果为 null 则抛出 NullPointerException
  2. 计算键的哈希值和桶的索引。
  3. 遍历链表,检查键是否已经存在,如果存在则更新值并返回旧值。
  4. 如果键不存在,调用 addEntry 方法添加新的键值对。
  5. addEntry 方法中,检查是否需要扩容,如果需要则调用 rehash 方法进行扩容。
  6. 创建新的 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 方法的主要步骤如下:

  1. 计算键的哈希值和桶的索引。
  2. 遍历链表,检查键是否存在,如果存在则返回对应的值,否则返回 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 方法的主要步骤如下:

  1. 计算键的哈希值和桶的索引。
  2. 遍历链表,找到要删除的键值对。
  3. 如果找到,将该节点从链表中移除,并更新 modCountcount
  4. 返回被删除的值,如果未找到则返回 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 方法的主要步骤如下:

  1. 计算新的容量,新容量为旧容量的 2 倍 + 1。
  2. 创建一个新的数组,大小为新容量。
  3. 遍历旧数组,将每个链表中的节点重新计算哈希值和索引,插入到新数组中。
  4. 更新 thresholdtable
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 的原理和性能特点,有助于我们在实际开发中更好地利用它来解决各种问题。

相关推荐
启航挨踢31 分钟前
java学习电子书推荐
java
wgslucky31 分钟前
Dubbo报错:module java.base does not “opens java.lang“ to unnamed module
java·开发语言·dubbo
DougLiang2 小时前
关于easyexcel动态下拉选问题处理
java·开发语言
mochensage2 小时前
C++信息学竞赛中常用函数的一般用法
java·c++·算法
计蒙不吃鱼2 小时前
一篇文章实现Android图片拼接并保存至相册
android·java·前端
小海编码日记3 小时前
Java八股-JVM & GC
java
全职计算机毕业设计3 小时前
基于Java Web的校园失物招领平台设计与实现
java·开发语言·前端
东阳马生架构3 小时前
商品中心—1.B端建品和C端缓存的技术文档
java
Chan163 小时前
【 SpringCloud | 微服务 MQ基础 】
java·spring·spring cloud·微服务·云原生·rabbitmq
LucianaiB3 小时前
如何做好一份优秀的技术文档:专业指南与最佳实践
android·java·数据库