【集合篇05】:Hashtable和HashMap全方位对比分析
Hashtable与HashMap对比分析 摘要:本文从多维度对比Java中两种哈希表实现的主要差异。在线程安全性方面,Hashtable采用synchronized方法保证线程安全但性能较低,HashMap非线程安全但性能更优。HashMap允许null键值而Hashtable不允许。历史版本上,Hashtable是JDK1.0遗留类,HashMap是JDK1.2改进版本。迭代器方面,Hasht
·
文章目录
本文从 线程安全性、 性能、 特点、 历史版本、 继承关系、 扩容机制等方面的区别,详细对比
Hashtable 和 HashMap 两种 哈希表的实现类的区别
一、主要区别
Hashtable和HashMap都是用于存储键值对的哈希表实现,但它们在设计、功能和性能上存在显著差异
线程安全性
Hashtable:线程安全。 它的所有公共方法都使用synchronized关键字进行修饰,这意味着在任何给定时间,只有一个线程可以访问Hashtable的实例。这种粗粒度的锁机制在多线程环境下可以保证数据一致性,但会带来较大的性能开销HashMap:非线程安全。 它的方法没有synchronized修饰。在单线程环境下性能很高,但在多线程环境下,如果不进行外部同步控制,可能会导致数据不一致、死循环等问题
性能
Hashtable:性能相对较低。 由于其所有操作都进行了同步,在多线程并发访问时,线程会频繁地竞争同一把锁,导致性能下降HashMap:性能相对较高。 在单线程环境下,由于没有同步开销,其性能优于Hashtable。在并发场景下,通常会使用ConcurrentHashMap(而非HashMap加外部同步),它提供了更细粒度的锁机制,性能远优于Hashtable
允许null键和null值
Hashtable:键和值都不允许为null。 如果尝试存入null键或null值,会抛出NullPointerExceptionHashMap:键和值都允许为null。HashMap允许一个null键(且只有一个),以及任意数量的null值
历史版本与继承关系
Hashtable: 是Java早期(JDK 1.0)提供的集合类,属于遗留类。它继承自Dictionary抽象类,并实现了Map、Cloneable、Serializable接口HashMap: 是Java 2(JDK 1.2)引入的,是Hashtable的改进版本。它继承自AbstractMap抽象类,并实现了Map、Cloneable、Serializable接口
迭代器(Iterator)
Hashtable: 提供了Enumeration(旧版迭代器)和Iterator两种遍历方式。Enumeration不是fail-fast的,而Iterator是fail-fast的HashMap: 只提供Iterator遍历方式,并且是fail-fast的。这意味着在迭代过程中,如果集合的结构被修改(除了通过迭代器自身的remove()方法),会立即抛出ConcurrentModificationException
初始容量和扩容机制
Hashtable: 默认初始容量为11,负载因子(Load Factor)为0.75。扩容时,容量变为2*oldCapacity + 1HashMap: 默认初始容量为16(必须是2的幂),负载因子为0.75。扩容时,容量变为2*oldCapacity
总结表格
| Hashtable | HashMap | |
|---|---|---|
| 线程安全 | 线程安全 (所有方法synchronized) |
非线程安全 |
| 性能 | 较低 (锁粒度粗) | 较高 (无同步开销) |
| Null键/值 | 不允许 null键和null值 |
允许 null键 (一个) 和 null值 (多个) |
| 历史版本 | JDK 1.0 遗留类 | JDK 1.2 引入,Hashtable的改进 |
| 继承体系 | 继承Dictionary,实现Map |
继承AbstractMap,实现Map |
| 迭代器 | Enumeration (非fail-fast) 和 Iterator (fail-fast) |
Iterator (fail-fast) |
| 默认容量 | 11 | 16 (2的幂) |
| 扩容方式 | 2*oldCapacity + 1 |
2*oldCapacity |
| 推荐使用 | 几乎不推荐,除非是遗留代码或特定要求 | 单线程环境下首选,多线程用ConcurrentHashMap |
二、扩容机制
HashMap 的扩容机制
- 默认初始容量:
HashMap的默认初始容量是16。当你创建一个HashMap实例时,内部的数组table并不会立即创建,而是等到第一次调用put()方法时才会被初始化为16 - 负载因子(Load Factor): 默认负载因子是0.75。它表示在哈希表自动扩容之前,可以达到多满
- 扩容阈值(Threshold): 扩容阈值 = 容量 × 负载因子。例如,初始容量16 * 0.75 = 12。当
HashMap中存储的元素数量(size)超过这个阈值时,就会触发扩容 - 扩容触发条件:
- 当前存储的元素数量
size> 扩容阈值threshold - 插入元素时发生哈希冲突,并且链表长度达到
TREEIFY_THRESHOLD(8),同时当前容量capacity小于MIN_TREEIFY_CAPACITY(64),这时会优先扩容而不是直接树化
- 当前存储的元素数量
- 扩容过程:
- 创建新数组: 创建一个新的、更大的数组。新数组的容量通常是原容量的两倍(
oldCapacity << 1) - 元素迁移(Rehashing): 遍历旧数组中的所有桶(包括链表和红黑树),将每个元素重新计算哈希值(因为数组长度变了,
index = hash & (newCapacity - 1)会得到不同的索引),然后将其放到新数组的正确位置- JDK 1.7 及之前: 链表中的元素在迁移时,可能会出现头插法导致的死循环问题(在多线程并发扩容时)
- JDK 1.8 及之后: 链表中的元素在迁移时,采用尾插法,并且由于新容量是旧容量的两倍,元素的新的索引要么是原索引,要么是原索引 +
oldCapacity。这大大简化了迁移过程,避免了死循环,并提升了效率。链表元素会根据其哈希值的某个位是0还是1,分别放入新数组的两个位置,形成两个新的链表(或树)
- 创建新数组: 创建一个新的、更大的数组。新数组的容量通常是原容量的两倍(
- 延迟初始化:
HashMap的底层数组table是在第一次put操作时才进行初始化的,而不是在构造函数中
关于更加详细的 HashMap 扩容及源码剖析佐证,可以阅读上一篇博客:【集合篇04】:HashMap扩容机制,resize()源码解读_hashmap resize-CSDN博客
Hashtable 的扩容机制
- 默认初始容量:
Hashtable的默认初始容量是11 - 负载因子(Load Factor): 默认负载因子是0.75
- 扩容阈值: 扩容阈值 = 容量 × 负载因子
- 扩容过程:
- 创建新数组: 创建一个新的数组。新数组的容量是原容量的两倍加1(
2 * oldCapacity + 1) - 元素迁移(Rehashing): 遍历旧数组,将所有元素重新计算哈希值并放到新数组的正确位置。这个过程同样会涉及到元素的重新散列
- 创建新数组: 创建一个新的数组。新数组的容量是原容量的两倍加1(
总结表格对比
| HashMap | Hashtable | |
|---|---|---|
| 默认初始容量 | 16 (2的幂) | 11 |
| 负载因子 | 0.75 (默认) | 0.75 (默认) |
| 扩容阈值 | 容量 * 负载因子 | 容量 * 负载因子 |
| 新容量计算 | 原容量的两倍 (oldCapacity << 1) |
原容量的两倍加1 (2 * oldCapacity + 1) |
| 数组初始化 | 延迟初始化 (第一次put时) |
立即初始化 (构造函数时) |
| 元素迁移 | JDK 1.8 优化,链表元素按位拆分,避免死循环 | 简单重新哈希 |
| 线程安全 | 非线程安全 (扩容过程无同步,多线程可能数据丢失) | 线程安全 (扩容过程也同步,性能开销大) |
更多推荐

所有评论(0)