深度解析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 的原理和性能特点,有助于我们在实际开发中更好地利用它来解决各种问题。

相关推荐
sleepcattt12 分钟前
Spring中Bean的实例化(xml)
xml·java·spring
lzzy_lx_208929 分钟前
Spring Boot登录认证实现学习心得:从皮肤信息系统项目中学到的经验
java·spring boot·后端
Dcs31 分钟前
立即卸载这些插件,别让它们偷你的资产!
java
小七mod40 分钟前
【Spring】Java SPI机制及Spring Boot使用实例
java·spring boot·spring·spi·双亲委派
Kotlin上海用户组1 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
亿.61 小时前
【Java安全】RMI基础
java·安全·ctf·rmi
zzq19961 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸1 小时前
Flutter 生命周期完全指南
android·flutter·ios
ruan1145141 小时前
Java Lambda 类型推断详解:filter() 方法与 Predicate<? super T>
java·开发语言·spring·stream
朱杰jjj1 小时前
解决jenkins的Exec command命令nohup java -jar不启动问题
java·jenkins·jar