Seal^_^头像
关注
Java 中 HashMap 和 Hashtable 的核心区别:从底层实现到线程安全全面对比封面图

Java 中 HashMap 和 Hashtable 的核心区别:从底层实现到线程安全全面对比


🌺The Begin🌺点点关注,收藏不迷路🌺

在 Java 集合框架中,HashMapHashtable 都是常用的键值对存储结构,名字相似,功能也相似。但它们之间存在着从设计思想、线程安全、性能表现到使用场景的全方位差异。很多新手容易混淆,面试中也几乎是必考题。

本文将深入剖析 HashMap 和 Hashtable 的八大核心区别,通过源码分析、流程图、性能对比和最佳实践,帮你彻底分清这对“双胞胎”。

1. 继承体系与历史渊源

«interface»

Map<K,V>

«abstract»

AbstractMap<K,V>

HashMap<K,V>

LinkedHashMap<K,V>

«abstract»

Dictionary<K,V>

Hashtable<K,V>

Properties

历史渊源

  • Hashtable:JDK 1.0 就存在,属于“元老级”类,命名遵循 Java 命名规范(驼峰)
  • HashMap:JDK 1.2 引入,作为 Map 接口的标准实现,取代 Hashtable

2. 八大核心区别详解

2.1 区别一:线程安全性

线程安全实现方式性能
Hashtable✅ 安全方法加 synchronized较差
HashMap❌ 不安全无锁较好

Hashtable 源码

public synchronized V put(K key, V value) {
    // 方法级别同步锁
}

public synchronized V get(Object key) {
    // 方法级别同步锁
}

HashMap 源码

public V put(K key, V value) {
    // 无锁,多线程下可能出问题
    return putVal(hash(key), key, value, false, true);
}

并发问题演示

Hashtable 安全

put 获得锁

等待锁

安全

线程3

Hashtable

线程4

串行执行

HashMap 并发问题

同时put

同时put

可能

JDK8

线程1

HashMap

线程2

死循环/数据丢失
JDK7 头插法

数据覆盖

2.2 区别二:键值允许 null 的情况

Key 可为 nullValue 可为 null
Hashtable❌ 不允许❌ 不允许
HashMap✅ 允许(1个)✅ 允许(多个)

源码验证

// Hashtable put 方法
public synchronized V put(K key, V value) {
    // 值不能为 null
    if (value == null) {
        throw new NullPointerException();
    }
    
    // 键不能为 null(调用 key.hashCode() 会 NPE)
    int hash = key.hashCode();  // key 为 null 时抛异常
}

// HashMap put 方法
public V put(K key, V value) {
    // 允许 null key 和 null value
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    // key 为 null 时返回 0
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

示例代码

// ✅ HashMap 可以
HashMap<String, String> map = new HashMap<>();
map.put(null, "nullKey");      // OK
map.put("key", null);           // OK
map.put(null, "another");       // 覆盖之前的 null key

// ❌ Hashtable 不行
Hashtable<String, String> table = new Hashtable<>();
table.put(null, "value");       // NullPointerException
table.put("key", null);         // NullPointerException

2.3 区别三:继承的父类不同

// HashMap 继承 AbstractMap
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

// Hashtable 继承 Dictionary(已过时的抽象类)
public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, Serializable

Dictionary 类:JDK 1.0 的遗留类,已被 Map 接口取代,不建议使用。

2.4 区别四:默认初始容量与扩容机制

对比项HashMap (JDK8)Hashtable
默认初始容量1611
扩容倍数2 倍(capacity << 1)2 倍 + 1(old*2+1)
容量要求2 的幂次方任意整数
扩容阈值计算threshold = capacity * loadFactor相同

HashMap 扩容源码

final Node<K,V>[] resize() {
    int newCap = oldCap << 1;  // 2 倍扩容
    // 保持容量为 2 的幂
}

static final int tableSizeFor(int cap) {
    // 返回大于等于 cap 的最小 2 的幂
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

Hashtable 扩容源码

protected void rehash() {
    int oldCapacity = table.length;
    int newCapacity = (oldCapacity << 1) + 1;  // 2倍+1
    // 不要求 2 的幂
}

扩容对比表

初始容量第1次扩容后第2次扩容后
HashMap(16)3264
Hashtable(11)2347

2.5 区别五:哈希计算方式不同

// HashMap (JDK8) - 扰动函数
static final int hash(Object key) {
    int h;
    // 高16位异或低16位,减少碰撞
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 计算索引: (n - 1) & hash

// Hashtable - 直接使用 hashCode
int hash = key.hashCode();
// 计算索引: (hash & 0x7FFFFFFF) % tab.length
int index = (hash & 0x7FFFFFFF) % table.length;

哈希计算流程图

Hashtable

Key

hashCode

& 0x7FFFFFFF
取绝对值

% table.length
取模运算 较慢

HashMap

Key

hashCode

h >>> 16

^ 异或

& n-1
位运算 高效

2.6 区别六:遍历方式与迭代器

迭代器类型fail-fastEnumeration
HashMapIterator✅ 支持❌ 不支持
HashtableIterator + Enumeration✅ Iterator 支持✅ 支持(遗留)

示例代码

// Hashtable 支持两种遍历
Hashtable<String, String> table = new Hashtable<>();

// 1. Enumeration(遗留方式)
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
    String key = keys.nextElement();
}

// 2. Iterator(推荐)
Iterator<Map.Entry<String, String>> it = table.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
}

2.7 区别七:性能对比

性能测试(10 万元素,JDK 17):

操作HashMap (ms)Hashtable (ms)Hashtable 慢多少
put(单线程)451182.6x
get(单线程)38952.5x
遍历2224相近

性能对比

快 2.6倍

快 2.5倍

HashMap put: 45ms

Hashtable put: 118ms

HashMap get: 38ms

Hashtable get: 95ms

2.8 区别八:ConcurrentModificationException

// HashMap - 快速失败(fail-fast)
HashMap<String, String> map = new HashMap<>();
map.put("a", "1");
for (String key : map.keySet()) {
    map.put("b", "2");  // ❌ ConcurrentModificationException
}

// Hashtable - 同样支持 fail-fast
Hashtable<String, String> table = new Hashtable<>();
for (String key : table.keySet()) {
    table.put("b", "2");  // ❌ 也会抛异常
}

3. 完整对比总结表

对比维度HashMapHashtable
引入版本JDK 1.2JDK 1.0
父类AbstractMapDictionary(过时)
线程安全❌ 不安全✅ 安全(synchronized)
null key✅ 允许(1个)❌ 不允许
null value✅ 允许(多个)❌ 不允许
默认容量1611
扩容方式2 倍2 倍 + 1
容量要求2 的幂次方任意
哈希计算扰动函数 + 位运算hashCode + 取模
迭代器Iterator(fail-fast)Iterator + Enumeration
性能较低
推荐使用✅ 是(需同步时手动加锁)❌ 不推荐(遗留类)

4. 并发场景下的替代方案

4.1 方案对比

方案线程安全性能推荐场景
Hashtable❌ 不推荐
Collections.synchronizedMap简单需求
ConcurrentHashMap强烈推荐

4.2 代码示例

// ❌ 不推荐:Hashtable(遗留,性能差)
Map<String, String> bad = new Hashtable<>();

// ⚠️ 可以但不够好:同步包装器
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

// ✅ 最佳实践:ConcurrentHashMap(JDK1.5+)
Map<String, String> best = new ConcurrentHashMap<>();

// 单线程下直接用 HashMap
Map<String, String> single = new HashMap<>();

4.3 选型决策流程图

否 低并发

需要使用 Map

是否多线程环境?

HashMap
性能最优

是否需要高并发读写?

ConcurrentHashMap
分段锁/CAS 高性能

是否需要 null key/value?

Collections.synchronizedMap
包装 HashMap

Hashtable

5. 常见面试追问

Q1:为什么 Hashtable 不允许 null?

A:历史遗留设计选择。JDK 1.0 的设计者认为 null 不是合法的键值。而 HashMap 的设计者认为 null 有意义(表示不存在)。

Q2:ConcurrentHashMap 如何做到比 Hashtable 性能更好?

A

  • Hashtable:整个表一把锁,任何操作都锁全表
  • ConcurrentHashMap (JDK8):CAS + synchronized 锁桶(链表头),并发度大大提升

Q3:HashMap 在 JDK7 和 JDK8 中有什么区别?

版本数据结构扩容时死循环问题
JDK7数组 + 链表头插法❌ 存在
JDK8数组 + 链表 + 红黑树尾插法✅ 解决

Q4:如何让 HashMap 线程安全?

三种方式

// 1. 包装(推荐度:中)
Map<String, String> map1 = Collections.synchronizedMap(new HashMap<>());

// 2. 使用 ConcurrentHashMap(推荐度:高)
Map<String, String> map2 = new ConcurrentHashMap<>();

// 3. 使用 Hashtable(推荐度:低)
Map<String, String> map3 = new Hashtable<>();

6. 最佳实践建议

6.1 代码规范

// ✅ 单线程 - 直接用 HashMap
Map<String, User> userCache = new HashMap<>();

// ✅ 多线程 - ConcurrentHashMap
Map<String, Session> sessionMap = new ConcurrentHashMap<>();

// ❌ 不要再用 Hashtable(除非维护老项目)
Map<String, Object> legacy = new Hashtable<>();  // 不推荐

// ❌ 不要用 Collections.synchronizedMap 除非简单场景
// 因为它只是简单包装,迭代时仍需手动同步
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 迭代时必须同步
synchronized (syncMap) {
    for (String key : syncMap.keySet()) {
        // ...
    }
}

6.2 一句话记忆

HashMap 线程不安全,性能高,允许 null;Hashtable 线程安全,性能低,不允许 null;高并发场景用 ConcurrentHashMap。

口诀

HashMap 单兵强,有 null 且快; 
Hashtable 老古董,全程锁慢慢来;
并发场景不用愁,ConcurrentHashMap 最实在。

7. 源码级对比速查表

特性HashMap (JDK8)Hashtable
底层结构数组 + 链表 + 红黑树数组 + 链表
树化阈值TREEIFY_THRESHOLD = 8不支持
默认负载因子0.75f0.75f
最大容量1 << 30Integer.MAX_VALUE
获取索引(n - 1) & hash(hash & 0x7FFFFFFF) % tab.length

如果你搞清楚了 HashMap 和 Hashtable 的区别,欢迎点赞、收藏、转发!有任何疑问,评论区一起交流~

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_41840843/article/details/161387758

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--