🌺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 可为 null Value 可为 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
默认初始容量 16 11
扩容倍数 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) 32 64
Hashtable(11) 23 47

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-fast Enumeration
HashMap Iterator ✅ 支持 ❌ 不支持
Hashtable Iterator + 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(单线程) 45 118 2.6x
get(单线程) 38 95 2.5x
遍历 22 24 相近

性能对比

快 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. 完整对比总结表

对比维度 HashMap Hashtable
引入版本 JDK 1.2 JDK 1.0
父类 AbstractMap Dictionary(过时)
线程安全 ❌ 不安全 ✅ 安全(synchronized)
null key ✅ 允许(1个) ❌ 不允许
null value ✅ 允许(多个) ❌ 不允许
默认容量 16 11
扩容方式 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.75f 0.75f
最大容量 1 << 30 Integer.MAX_VALUE
获取索引 (n - 1) & hash (hash & 0x7FFFFFFF) % tab.length

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

在这里插入图片描述


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

更多推荐