java集合之HashTable分析

本文详细剖析了Java中的HashTable,介绍了其底层实现、重要参数、构造函数、插入与读取方法,以及与HashMap和ConcurrentHashMap的联系与区别。HashTable是线程安全的,但效率相对较低,适用于并发环境,而HashMap是非线程安全但效率更高的选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

Hashtable 与 HashMap 都是 Map 族中较为常用的实现,也都是 Java Collection Framework 的重要成员,它们的本质都是 链表数组。

Hashtable 和 HashMap 既是 Java Collection Framework 的重要成员,也是 Map 族(如下图所示)的核心成员,二者的底层实现都是一个链表数组,具有寻址容易、插入和删除也容易的特性。事实上 HashMap 几乎可以等价于 Hashtable,除了 HashMap 是非线程安全的并且可以接受 null 键和 null 值。

基于jdk1.8

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
	...
}

HashTable底层实现

重要参数

public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
 
    private transient Entry[] table;   // 由Entry对象组成的链表数组
 
    private transient int count;   // Hashtable中Entry对象的个数
 
    private int threshold;   // Hashtable进行扩容的阈值
 
    private float loadFactor;   // 在其容量自动增加之前可以达到多满的一种尺度,默认为0.75
 
    private transient int modCount = 0;   // 记录Hashtable生命周期中结构性修改的次数
}

与 HashMap 类似,Hashtable 也包括五个成员变量,分别是 table 数组、Hashtable 中 Entry 个数 count、Hashtable 的阈值 threshold、Hashtable 的负载因子 loadFactor 和 Hashtable 结构性修改次数 modCount。下面分别给出这五个成员的具体内涵:

  • Entry 数组 table: 一个由 Entry 对象组成的链表数组,table 数组的每一个数组成员就是一个链表;
  • Entry 个数 count: Hashtable 中 Entry 对象的个数;
  • 阈值 threshold: Hashtable 进行扩容的阈值;
  • 负载因子 loadFactor: 在其容量自动增加之前可以达到多满的一种尺度,默认为0.75;
  • 结构性修改次数 modCount: 记录 Hashtable 生命周期中结构性修改的次数,便于快速失败(所谓快速失败是指其在并发环境中进行迭代操作时,若其他线程对其进行了结构性的修改,这时迭代器能够立马感知到并且立即抛出 ConcurrentModificationException 异常,而不是等到迭代完成之后才告诉你(你已经出错了));

底层结构

Hashtable 和 HashMap 本质上都是一个链表数组。数组中存放的元素是 Entry 对象,而 Entry 对象是一种典型链状结构,定义如下:

static class Entry<K,V> implements Map.Entry<K,V> {
 
    K key;     // 键值对的键
    V value;        // 键值对的值
    Entry<K,V> next;     // 指向下一个节点的指针
    int hash;     // key 的哈希值(与HashMap中key的哈希值计算方式不同)
 
    /**
     * Creates new entry.
     */
    protected Entry(int h, K k, V v, Entry<K,V> n) {     // Entry 的构造函数
        value = v;
        next = n;
        key = k;
        hash = h;
    }
}

Entry 为 Hashtable 的内部类,实现了 Map.Entry 接口,是个典型的四元组,包含了键 key、值 value、指向下一个节点的指针 next,以及 Key 的 hash 值四个属性。事实上 Entry 是构成哈希表的基石,是哈希表所存储的元素的具体形式

重要方法

构造函数

Hashtable 一共提供了四个构造函数,其中默认无参的构造函数和参数为Map的构造函数为 Java Collection Framework 规范的推荐实现,
其余两个构造函数则是Hashtable专门提供的。

 //该构造函数意在构造一个指定初始容量和指定负载因子的空 Hashtable
    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;  // 初始容量完全由用户随意指定,不必是2的n次幂(不同于HashMap)
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];   // 创建指定大小为initialCapacity的链表数组
        threshold = (int) (initialCapacity * loadFactor);   // HashTable的扩容阈值
    }
 
    //该构造函数意在构造一个具有默认初始容量(11)和默认负载因子(0.75f)的空 Hashtable,是 Java Collection Framework 规范推荐提供的
    public Hashtable() {
        this(11, 0.75f);   // 默认容量是11,不同于HashMap的默认初始容量16,默认负载因子0.75
    }
 
    //该构造函数意在构造一个指定初始容量和默认负载因子(0.75f)的空 Hashtable
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
 
    //该构造函数意在构造一个与指定 Map 具有相同映射的 Hashtable,其初始容量不小于11(具体依赖于指定Map的大小),负载因子是0.75f,
    //是 Java Collection Framework 规范推荐提供的
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2 * t.size(), 11), 0.75f);
        putAll(t);
    }

与 HashMap 类似,构建一个 Hashtable 时也需要指定初始容量和负载因子这两个非常重要的参数,它们是影响 Hashtable 性能的关键因素。其中,容量表示哈希表中桶的数量(table 数组的大小),初始容量是创建哈希表时桶的数量;负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

对于 Hashtable 而言,查找一个元素的平均时间是 O(1+a) (a 指的是链的长度,是一个常数)。特别地,负载因子越大,对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子为0.75f,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。

插入put()方法

 在Hashtable中,键值对的存储是也是通过 put(key, vlaue) 方法来实现的,不同于 HashMap 的是,其 put 操作是线程安全的,源码如下:

 public synchronized V put(K key, V value) {     // 加锁同步,保证Hashtable的线程安全性
        // Make sure the value is not null
        if (value == null) {      // 不同于HashMap,Hashtable不允许空的value
            throw new NullPointerException();
        }
 
        // Makes sure the key is not already in the hashtable.
        Entry tab[] = table;
        int hash = key.hashCode();   // key 的哈希值,同时也暗示Hashtable不同于HashMap,其不允许空的key
        int index = (hash & 0x7FFFFFFF) % tab.length;   // 取余计算节点存放桶位,0x7FFFFFFF 是最大的int型数的二进制表示
        // 先查找Hashtable上述桶位中是否包含具有相同Key的K/V对
        for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }
 
        // 向Hashtable中插入目标K/V对
        modCount++;     // 发生结构性改变,modCount加1
        if (count >= threshold) {    //在插入目标K/V对前,先检查是否需要扩容(不同于HashMap的插入后检查是否需要扩容) 
            // Rehash the table if the threshold is exceeded
            rehash();
 
            tab = table;
            index = (hash & 0x7FFFFFFF) % tab.length;   // 扩容后,重新计算K/V对插入的桶位
        }
 
        // Creates the new entry.
        Entry<K, V> e = tab[index];
        tab[index] = new Entry<K, V>(hash, key, value, e); // 将K/V对链入对应桶中链表,并成为头结点
        count++;     // Hashtable中Entry数目加1
        return null;
    }

通过上述源码我们可以看出,Hashtable 与 HashMap 保存数据的过程基本相同:
首先,计算 key 的 hash 值并确定 K/V 对要插入的桶位;
其次,查找该桶位中是否存在具有相同的 key 的 K/V 对,若存在则覆盖直接对应的 value 值,
否则将该节点(K/V)保存在桶中的链表的链头位置(最先保存的元素放在链尾)。
当然,若该桶位是空的,则直接保存。

特别地,在一些细节上,Hashtable 与 HashMap 还是有一定的差别的:

  • Hashtable不同于HashMap,前者既不允许key为null,也不允许value为null;
  • HashMap中用于定位桶位的Key的hash的计算过程要比Hashtable复杂一点,没有Hashtable如此简单、直接;
  • 在HashMap的插入K/V对的过程中,总是先插入后检查是否需要扩容;而Hashtable则是先检查是否需要扩容后插入;
  • Hashtable不同于HashMap,前者的put操作是线程安全的。

rehash操作

重哈希过程主要是一个重新计算原 Hashtable 中的元素在新 table 数组中的位置并进行复制处理的过程,我们直接看其源码:

   protected void rehash() {
        int oldCapacity = table.length;   // 先获取旧的Hashtable桶的数量,即容量
        Entry[] oldMap = table;
 
        int newCapacity = oldCapacity * 2 + 1;    // 扩容,扩到原始容量的2倍再加1
        Entry[] newMap = new Entry[newCapacity];   // 创建扩容后的新的链表数组
 
        modCount++;                // 重哈希操作是一个结构性改变操作,modCount加1
        threshold = (int) (newCapacity * loadFactor);   // 新的阈值   
        table = newMap;
 
        // 将原哈希表中的节点逐个复制到新的哈希表中
        for (int i = oldCapacity; i-- > 0;) {
            for (Entry<K, V> old = oldMap[i]; old != null;) {
                Entry<K, V> e = old;
                old = old.next;
 
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }

特别需要注意的是,在重哈希的过程中,原属于同一个桶中的 Entry 对象可能会被分到不同的桶,
因为 Hashtable 的容量发生了变化,那么(e.hash & 0x7FFFFFFF) % newCapacity 的值也会发生相应的变化。
退一步说,如果重哈希后原属于一个桶中的 Entry 对象仍属于同一桶,那么重哈希也就失去了意义。

读取get()方法实现

Hashtable 只需通过 key 的 hash 值定位到 table 数组的某个特定的桶,然后查找并返回该 key 对应的 value 即可,源码如下:

public synchronized V get(Object key) {    // 不同于HashMap,Hashtable的读取操作是同步的
        Entry tab[] = table;
        int hash = key.hashCode();   
        int index = (hash & 0x7FFFFFFF) % tab.length;   // 定位K/V对的桶位
        for (Entry<K, V> e = tab[index]; e != null; e = e.next) {   // 在特定桶中依次查找指定Key的K/V对
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;       
            }
        }
        return null;   // 查找失败
    }

在这里能够根据 key 快速的取到 value,除了和 Hashtable 的数据结构密不可分外,还和 Entry 有莫大的关系。
在前面就已经提到过,Hashtable 在存储过程中并没有将 key,value 分开来存储,而是当做一个整体 Entry 对象(四元组)来处理的。
可以看到,在 Entry 对象中,value 的地位要比 key 低一些,相当于是 key 的附属。在读取细节上,Hashtable 与 HashMap 的主要差别如下:

  • 不同于 HashMap,Hashtable 的读取操作是同步的;
  • 在 HashMap 中,若读取到的 Value 值为 NULL,则存在如下两种可能:该 key 对应的值就是 null 或者 HashMap 中不存在该 key;而在 Hashtable 中却只有一种可能:Hashtable 中不存在含有该 key 的 Entry。造成这种差别的原因正是二者对 Key 和 Value 的限制的不同:HashMap 最多允许一个 Key 为 null,但允许多个 value 值为 null;Hashtable 既不允许空的 Key,也不允许空的 Value。

HashMap、Hashtable 与 ConcurrentHashMap 的联系与区别

Hashtable 与 HashMap 的联系与区别

  (1) HashMap 和 Hashtable 的实现模板不同:虽然二者都实现了 Map 接口,但 HashTable 继承于 Dictionary 类,而 HashMap 是继承于 AbstractMap。Dictionary 是是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的骨干实现,它以最大限度地减少实现此接口所需的工作。

  (2) HashMap 和 Hashtable 对键值的限制不同:HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为 null。

  (3) HashMap和Hashtable的线程安全性不同:Hashtable 的方法是同步的,实现线程安全的 Map;而 HashMap 的方法不是同步的,是Map的非线程安全实现。

  (4) HashMap 和 Hashtable 的地位不同:在并发环境下,Hashtable 虽然是线程安全的,但是我们一般不推荐使用它,因为有比它更高效、更好的选择 ConcurrentHashMap;而单线程环境下,HashMap 拥有比 Hashtable 更高的效率(Hashtable的操作都是同步的,导致效率低下),所以更没必要选择它了。

Hashtable 与 ConcurrentHashMap 的联系与区别

  • Hashtable 和 ConcurrentHashMap 都可以用于并发环境,但是 Hashtable 的并发性能远不如 ConcurrentHashMap。
    这种差异是由它们的底层实现决定的。
    我们知道 ConcurrentHashMap 引入了分段锁机制,在默认理想状态下,ConcurrentHashMap 可以支持16个线程执行并发写操作及任意数量线程的读操作;而 Hashtable 无论在读的过程中还是写的过程中都会锁定整个 map,因此在并发效率上远不如ConcurrentHashMap。

  • 此外,Hashtable 和 ConcurrentHashMap 对键值的限制相同,二者的 key 和 value 都不允许是 null。

<think>嗯,用户让我介绍一下Java集合篇中的HashTable,还要进行源码分析。首先,我需要回忆一下HashTable的基本概念和它在Java集合框架中的位置。HashTable是一个古老的类,从Java 1.0开始就有了,但后来被HashMap取代了,不过它和ConcurrentHashMap有什么区别呢?用户可能想知道这些对比。 接下来,我应该先概述HashTable的特点,比如线程安全,使用synchronized方法,不允许null键和null值,初始容量和加载因子这些参数。然后对比HashMap,说明为什么现在推荐使用ConcurrentHashMap或者Collections.synchronizedMap来替代HashTable,这样用户能理解其优缺点。 然后,用户可能需要源码分析,我需要回忆HashTable的结构,比如内部用Entry数组实现,如何处理哈希冲突,扩容机制是怎样的。特别是要详细讲解put、get、rehash这些方法的实现,注意里面的同步机制,比如方法级别的synchronized,这会影响性能。 可能用户对HashTable的线程安全实现方式感兴趣,可以解释每个方法都用synchronized修饰,虽然保证了线程安全,但在高并发下性能差。对比ConcurrentHashMap的分段锁,说明为什么后者更好。 另外,HashTable的枚举器(Enumerator)和迭代器的区别,以及快速失败机制也需要提到,这部分用户可能在使用中遇到过并发修改异常,可以举例说明。 还要注意HashTable的初始容量是11,而HashMap是16,为什么有这样的区别,可能和哈希函数有关,比如HashTable使用取模运算,而HashMap使用位运算,所以容量设计不同。 在源码分析部分,可能需要逐行解释put方法,比如如何计算索引,处理冲突,检查是否重复,以及扩容的条件。rehash方法中的新容量计算是2倍加1,这也是一个特点,可以说明原因,比如保持素数,减少哈希冲突。 最后,总结部分要强调HashTable的现状,虽然现在不推荐使用,但理解它的实现有助于学习其他集合类,比如ConcurrentHashMap的设计思路。同时提醒用户在实际开发中的替代方案,避免误用。 需要检查是否有遗漏点,比如HashTable的继承结构,实现了Map接口,继承自Dictionary类,而HashMap继承自AbstractMap。这点可能对理解它的设计有帮助。此外,HashTable的key的hashCode计算直接使用对象的hashCode,而HashMap在JDK8中做了优化,处理哈希冲突时使用红黑树代替链表,这也是为什么HashMap性能更好的原因之一。 可能用户还希望看到示例代码,比如基本的用法,以及多线程下的问题,但问题中没有明确要求,可能不需要详细展开,但可以简单提及。总之,结构要清晰,先介绍特性,再源码分析,对比其他类,最后总结应用场景和替代方案。</think>### Java HashTable 解析与源码分析 HashTableJava 早期(JDK 1.0)提供的**线程安全**的键值对集合,基于“数组 + 链表”实现,与 `HashMap` 类似但设计上存在显著差异。以下是关键点解析与源码分析: --- ### **一、核心特性** 1. **线程安全** - 所有方法均通过 `synchronized` 关键字修饰,保证多线程环境下的同步访问。 - **缺点**:高并发下性能较差(锁粒度粗,锁整个实例)。 2. **不允许 `null` 键/值** - `put(null, value)` 或 `get(null)` 会直接抛出 `NullPointerException`。 - 与 `HashMap` 不同,后者允许单个 `null` 键和多个 `null` 值。 3. **初始容量与扩容** - 默认初始容量为 **11**,负载因子 **0.75**。 - 扩容阈值:`容量 * 负载因子`。扩容时,新容量为 `原容量 * 2 + 1`(保持为奇数/素数,减少哈希冲突)。 --- ### **二、源码核心结构分析** #### 1. **内部存储结构:Entry 数组** ```java private transient Entry<?,?>[] table; // 存储键值对的数组 static class Entry<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Entry<K,V> next; // 链表结构解决哈希冲突 } ``` #### 2. **哈希计算与索引定位** - **哈希计算**:直接使用 `key.hashCode()`。 - **索引定位**:`(hash & 0x7FFFFFFF) % table.length`(通过取模确保索引非负)。 #### 3. **关键方法源码解析** **(1)`put(K key, V value)` 方法** ```java public synchronized V put(K key, V value) { if (value == null) throw new NullPointerException(); Entry<?,?> tab[] = table; int hash = key.hashCode(); // 计算哈希 int index = (hash & 0x7FFFFFFF) % tab.length; // 计算索引 // 遍历链表,检查是否已存在相同key 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); // 新增Entry return null; } ``` **(2)`get(Object key)` 方法** ```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; } ``` **(3)扩容机制 `rehash()`** ```java protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; int newCapacity = (oldCapacity << 1) + 1; // 新容量 = 原容量*2 +1 Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; // 迁移旧数据到新数组 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; } } table = newMap; } ``` --- ### **三、HashTable 的局限性** 1. **性能问题** - 全局锁导致并发效率低,推荐使用 `ConcurrentHashMap`(分段锁或 CAS 优化)。 2. **设计过时** - 继承自 `Dictionary` 抽象类(已标记为过时),而非 `Map` 接口。 - 迭代器 `Enumerator` 不支持快速失败(fast-fail)机制,而 `HashMap` 的迭代器支持。 --- ### **四、对比 HashMap** | 特性 | HashTable | HashMap | |---------------------|----------------------------|---------------------------| | **线程安全** | 是(方法级 `synchronized`) | 否(需外部同步) | | **Null 键/值** | 禁止 | 允许 | | **迭代器** | 使用 `Enumeration` | 使用 `Iterator` | | **哈希冲突解决** | 链表 | 链表 + 红黑树(JDK8+) | | **初始容量** | 11 | 16 | --- ### **五、总结** - **适用场景**:遗留系统或需要简单线程安全的场景(现代开发中建议用 `ConcurrentHashMap`)。 - **核心设计思想**:通过粗粒度锁保证线程安全,但牺牲了扩展性和性能。 - **学习价值**:理解早期 Java 集合的设计逻辑,对比 `ConcurrentHashMap` 的优化思路。 建议在实际开发中优先选择 `ConcurrentHashMap` 或 `Collections.synchronizedMap()` 替代 HashTable
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值