臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

HashMap 和 Hashtable 都用哈希表保存键值对,但 HashMap 非线程安全、允许 null,现代实现还会把过长链表转成红黑树;Hashtable 的主要方法使用 synchronized、不允许 null,属于较老的实现。

Java 中正确的类名是 Hashtable,不是 HashTable。

二、🧭 理论:它们是什么

2.1 先认识 Hashtable

Hashtable<K, V> 是 Java 早期提供的 Map 实现,用 Key 查找 Value,例如:

Map<String, Integer> scores = new Hashtable<>();
scores.put("语文", 90);
scores.put("数学", 95);
 
// get:根据 Key 取出对应的 Value。
System.out.println(scores.get("语文")); // 90

它和 HashMap 的基本用途相同,都适合表达“姓名 → 电话”“商品编号 → 库存”这样的对应关系。最大的特点是:主要方法自带同步控制,因此常被理解为“加了大锁的老版 HashMap”。

2.2 Hashtable 和哈希表不是同一个概念

  • **哈希表(hash table)**是一种数据结构;
  • Hashtable 是 Java 中一个具体的类;
  • HashMap 虽然类名不是 Hashtable,底层同样使用了哈希表。

因此,不能认为只有 Hashtable 才是哈希表。

2.3 为什么现在很少使用

Hashtable 出现得很早,后来才被纳入 Java Collections Framework。现代代码通常这样选择:

场景常见选择
不需要并发访问HashMap
多线程共享并修改ConcurrentHashMap
维护历史代码或调用老式 API可能遇到 Hashtable

你也可能间接使用过它:Properties 继承自 Hashtable<Object, Object>。

可以把 Map 想成一排储物柜:

  • hashCode():根据 Key 算出大致放进哪个柜子;
  • 桶(bucket):真正存放键值对的位置;
  • equals():同一个柜子里出现多个 Key 时,再确认是不是正在寻找的 Key。

类比的边界是:真实实现不是直接拿 hashCode() 当数组下标,还会经过扰动和取模或位运算。

2.4 主要区别

对比项HashMapHashtable
线程安全不保证线程安全主要公开方法使用 synchronized
null允许一个 null Key、多个 null ValueKey 和 Value 都不能为 null
底层结构数组 + 链表 + 红黑树(JDK 8+)数组 + 链表
默认容量1611
默认负载因子0.750.75
扩容方式通常扩大为原来的 2 倍通常扩大为原来的 2 倍再加 1
推荐场景普通 Map 的常用选择兼容旧代码,现代业务很少新用

线程安全不等于性能更好

Hashtable 通常给整个实例加锁,并发访问容易互相等待。需要高并发共享 Map 时,一般使用 ConcurrentHashMap。

三、⚙️ 理论:底层怎么工作

3.1 HashMap 的 put 流程

  1. 根据 Key 计算哈希值和数组下标。
  2. 桶为空,直接放入新节点。
  3. 桶不为空,通过 hash 和 equals() 判断 Key 是否已经存在。
  4. Key 已存在就覆盖 Value;否则把节点加入链表或红黑树。
  5. 元素数量超过阈值 容量 × 负载因子 时扩容。

JDK 8+ 中,一个桶里的节点达到树化条件时会转成红黑树。常见面试数值是:链表节点达到 8,并且数组容量至少为 64;容量不足 64 时优先扩容,而不是立即树化。

数组 table
  ├─ 桶 0 → null
  ├─ 桶 1 → Node → Node              链表
  └─ 桶 2 → TreeNode ↔ TreeNode      红黑树

3.2 Hashtable 的 put 流程

Hashtable 同样先计算哈希值和桶下标,再用链表解决哈希冲突。主要区别是:

  • put、get 等主要方法带有 synchronized,同一时刻通常只有一个线程能执行实例上的这些方法;
  • 不支持 null Key 和 null Value;
  • 冲突增多时仍使用链表,不会像现代 HashMap 那样树化。

可以把同步过程理解成:

线程 A:拿到同一个 Hashtable 的锁 → 执行 put → 释放锁
线程 B:                等待拿锁 → 执行 get → 释放锁

源码形式可以简化理解为:

public synchronized V put(K key, V value) {
    // synchronized:执行该方法前,线程要先获得当前 Hashtable 实例的锁。
    // 其他线程暂时不能同时执行这个实例上同样需要该锁的同步代码。
}

不过,单个方法同步不代表组合操作一定安全:

// containsKey 和 put 分别是同步方法,
// 但其他线程仍可能在两次方法调用之间插入操作。
if (!hashtable.containsKey("count")) {
    hashtable.put("count", 1);
}

如果需要原子地完成“判断后修改”,应使用合适的并发 API,或对整个组合操作额外同步。

共同点

两者都依赖 Key 正确实现 hashCode() 和 equals()。如果只重写其中一个,可能出现“看起来相同的 Key,却取不到 Value”的问题。

四、🚀 实践:从准备到验证

4.1 前置准备

  • JDK 8 或更高版本;
  • 不需要第三方依赖。

4.2 可以拿来干什么

  • 单线程、方法内临时数据或不共享的 Map:使用 HashMap。
  • 多线程只读:正确构建并安全发布后,可以读取 HashMap。
  • 多线程共享且会修改:优先考虑 ConcurrentHashMap,不要因为 Hashtable 名字里有 Table 就默认选择它。

4.3 完整实践:代码、启动和验证

新建 MapDifferenceDemo.java:

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
 
public class MapDifferenceDemo {
 
    public static void main(String[] args) {
        // HashMap 允许 null Key 和 null Value。
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(null, "空键");
        hashMap.put("name", null);
        System.out.println("HashMap:" + hashMap);
 
        // Hashtable 的主要方法有同步控制,但不允许 null。
        Map<String, String> hashtable = new Hashtable<>();
        hashtable.put("name", "Afyke");
        System.out.println("Hashtable:" + hashtable);
 
        try {
            // 这里会抛出 NullPointerException。
            hashtable.put(null, "空键");
        } catch (NullPointerException exception) {
            System.out.println("Hashtable 不允许 null Key");
        }
    }
}

运行:

javac MapDifferenceDemo.java
java MapDifferenceDemo

预期能看到 HashMap 保存了 null,而 Hashtable 在写入 null Key 时抛出 NullPointerException。Map 的打印顺序不固定,不影响结论。

五、🎤 面试时怎么流利地说

5.1 回答路线

共同点 → 线程安全与 null → 底层结构 → 使用选择。

5.2 30 秒标准回答

HashMap 和 Hashtable 都基于哈希表实现 Map。主要区别是 HashMap 非线程安全,允许一个 null Key 和多个 null Value;Hashtable 的主要方法使用 synchronized,Key 和 Value 都不能为 null。底层上,JDK 8 之后的 HashMap 是数组、链表和红黑树,冲突严重时可以树化;Hashtable 主要还是数组加链表。实际开发中,普通场景用 HashMap,并发场景通常用 ConcurrentHashMap,很少新用 Hashtable。

5.3 1—2 分钟展开回答

两者放入元素时,都会根据 Key 的 hashCode() 定位桶,再通过 equals() 判断 Key 是否相同。发生哈希冲突时,HashMap 先使用链表;JDK 8+ 满足条件后会转成红黑树,降低极端冲突下的查询成本。

Hashtable 通过方法级同步保证单次方法调用的线程安全,但锁的粒度较大,而且“先判断再修改”这类组合操作仍需要额外保证原子性。因此,不能只因为它线程安全就优先使用它。

5.4 高频追问与短答

5.4.1 为什么 HashMap 允许 null?

HashMap 对 null Key 做了专门处理,可以把它放到特定桶中。Hashtable 会直接拒绝 null Key 和 Value。

5.4.2 Hashtable 完全可以代替 ConcurrentHashMap 吗?

不建议。两者都能提供线程安全的 Map 操作,但 Hashtable 的锁粒度较大;ConcurrentHashMap 专门面向高并发设计,通常有更好的并发性能。

5.4.3 Hashtable 单个方法同步,组合操作也一定安全吗?

不一定。例如“先 containsKey,再 put”包含两次调用,其他线程可能在中间插入操作。需要原子组合操作时,应选择合适的并发 API 或额外同步。

六、📌 总结

  • HashMap:非线程安全、允许 null、JDK 8+ 支持链表树化。
  • Hashtable:主要方法同步、不允许 null、数组加链表。
  • 两者都依赖正确的 hashCode() 和 equals()。
  • 普通场景用 HashMap;并发修改优先考虑 ConcurrentHashMap。

一句话记忆

HashMap 灵活常用但不自带锁;Hashtable 自带大锁但已经偏老;并发优先 ConcurrentHashMap。

相关笔记:1.hashmap存储在方法内是线程安全的吗、../基础/1.HashMap、../基础/2.ConcurrentHashMap

七、📚 官方资料