HashMap、HashSet 和 Hashtable 有什么区别?
简化版
三者底层都用哈希表。HashMap 存「键值对」,线程不安全、允许一个 null 键和 null 值,是最常用的;HashSet 存「不重复的元素」,底层其实就是一个只用键的 HashMap;Hashtable 是远古遗留的键值对实现,方法都加了 synchronized(线程安全但慢)、不允许 null,现在基本被 ConcurrentHashMap 取代。
详细版
| 维度 | HashMap | HashSet | Hashtable |
|---|---|---|---|
| 存什么 | 键值对 key-value | 单个元素(去重) | 键值对 key-value |
| 底层 | 数组+链表+红黑树 | 内部持有一个 HashMap | 数组+链表 |
| 线程安全 | 否 | 否 | 是(方法 synchronized) |
| 允许 null | 1 个 null 键、多个 null 值 | 允许 1 个 null 元素 | 键值都不允许 null |
| 性能 | 快 | 快 | 慢(锁整表) |
| 出身 | Java 1.2,Map 接口 | Java 1.2,Set 接口 | Java 1.0,遗留类 |
核心记忆:HashSet = 只关心 key 的 HashMap;Hashtable = 加了锁的老古董,别用它,要线程安全用 ConcurrentHashMap。
完整版教学
一、HashMap:主角
HashMap<K,V> 存键值对,底层是「数组 + 链表 + 红黑树」(Java 8+)。特点:
- 线程不安全:并发 put 可能丢数据、扩容可能出问题,多线程别直接用。
- 允许 null:可以有 1 个
null键(放在下标 0 的桶),value 可以有多个 null。 - 遍历无序(顺序和插入无关,扩容后还会变)。
它是日常开发用得最多的 Map 实现。
二、HashSet:披着 Set 外衣的 HashMap
HashSet 内部就藏了一个 HashMap:你 add(e) 的元素被当作 HashMap 的 key,value 统一填一个固定的空对象 PRESENT。
// HashSet 内部
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null; // 复用 HashMap 的 key 去重
}
所以 HashSet 的「去重」本质就是 HashMap 的「key 唯一」。它天然继承了 HashMap 的一切特性:无序、线程不安全、允许一个 null 元素。判断重复靠 hashCode() + equals()。
三、Hashtable:不该再用的遗留类
Hashtable 是 Java 1.0 的老类,比集合框架还早。问题:
- 每个方法都
synchronized:锁的是整张表,并发时所有读写互相阻塞,性能很差。 - 不允许 null 键或 null 值:put null 直接抛
NullPointerException。 - 设计陈旧,已被官方标注为「若需线程安全,请用
ConcurrentHashMap」。
ConcurrentHashMap 用分段锁(Java 7)/ CAS + synchronized 桶级锁(Java 8)实现细粒度并发,比 Hashtable 锁整表高效得多。
四、为什么 HashMap 允许 null 而 Hashtable 不允许
Hashtable 认为 null 有歧义:get(key) 返回 null 时,分不清是「key 不存在」还是「值本身是 null」,且它的哈希直接调 key.hashCode(),null 会 NPE。HashMap 特殊处理了 null 键(固定放 0 号桶),并提供 containsKey 来区分「不存在」和「值为 null」,所以能容忍 null。
五、面试高频追问
- 要线程安全的 Map 用什么? →
ConcurrentHashMap(不是 Hashtable,也不是Collections.synchronizedMap,后者也是锁整表)。 - HashSet 怎么保证不重复? → 靠元素的
hashCode()定位桶 +equals()比较,两者都要正确重写。 - HashMap 和 HashSet 谁包含谁? → HashSet 内部用 HashMap 实现。
六、常见误区与追问
| 结构 | 底层关系 | 线程安全 | null 支持 |
|---|---|---|---|
| HashMap | key-value 哈希表 | 非线程安全 | 允许一个 null key,多个 null value |
| HashSet | 基于 HashMap 的 key | 非线程安全 | 可放一个 null 元素 |
| Hashtable | 老式同步哈希表 | 方法级同步 | 不允许 null key/value |
易错点:HashSet 不是另一套神秘结构,它主要是把元素当作 HashMap 的 key,value 使用一个占位对象。
数字例子:向 HashSet 放入 1000 个元素,本质上类似向内部 HashMap 放入 1000 个 key,每个 key 对应同一个 dummy value。判断元素是否存在,就是看内部 map 是否包含这个 key。因此 HashSet 的去重语义依赖元素的 hashCode 和 equals 是否正确。
- 误区:HashSet 比 HashMap 更底层。 HashSet 通常是基于 HashMap 封装出来的集合视图。
- 误区:Hashtable 线程安全所以现在更推荐。 它是遗留类,方法级同步粒度粗;并发场景通常考虑 ConcurrentHashMap。
- 误区:HashMap 的 key 相同只看 hashCode。 hashCode 定位桶,equals 确认逻辑相等,两者都重要。
- 追问:HashSet 如何保证不重复? 新元素作为 key 放入 HashMap,key 已存在时覆盖占位 value,不增加新元素。
- 追问:为什么重写 equals 要重写 hashCode? 相等对象必须有相同 hashCode,否则可能落到不同桶,集合语义会错。
- 追问:HashMap 能否在多线程下直接写? 不能保证线程安全,可能出现数据丢失、可见性问题或结构异常。
七、加强记忆
HashMap 存键值对、线程不安全、允许 null、最常用;HashSet 就是「只用 key 的 HashMap」,靠 key 唯一去重;Hashtable 是加了 synchronized、不许 null 的遗留类,性能差,要线程安全请改用 ConcurrentHashMap。三者底层都是哈希表。