Java 中 HashSet 和 HashMap 的核心区别:底层实现、存储结构与使用场景全解析
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
在 Java 集合框架中,HashSet 和 HashMap 是使用频率最高的两个类。它们的名字相似,底层都依赖哈希表,但一个是单列集合(存储单个元素),一个是双列集合(存储键值对)。很多初学者容易混淆,面试中也是高频考点。
本文将深入剖析 HashSet 和 HashMap 的本质区别,通过源码分析、结构图解、性能对比和选型指南,帮你彻底理清这对“亲戚”关系。
1. 核心关系:HashSet 底层就是 HashMap
最重要的结论:HashSet 的内部实现完全基于 HashMap。
// HashSet 源码(核心)
public class HashSet<E> {
private transient HashMap<E, Object> map; // 底层 HashMap
// 占位对象,所有 value 都指向它
private static final Object PRESENT = new Object();
public boolean add(E e) {
// 将元素作为 key 存入 HashMap,value 统一为 PRESENT
return map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
public boolean contains(Object o) {
return map.containsKey(o);
}
}
关系示意图:
渲染错误: Mermaid 渲染失败: Parse error on line 8: ...存储逻辑 Add[add('A')] --> Put[map.p ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
2. 八大核心区别详解
2.1 区别一:存储结构
| 类 | 存储内容 | 结构类型 | 元素数量维度 |
|---|---|---|---|
| HashMap | 键值对(Key-Value) | 双列集合(2列) | size = 键值对数量 |
| HashSet | 单个元素 | 单列集合(1列) | size = 元素数量 |
图解对比:
2.2 区别二:元素唯一性保证
| 类 | 唯一性约束 | 判断依据 | 重复处理 |
|---|---|---|---|
| HashMap | Key 唯一 | hashCode() + equals() | 新 value 覆盖旧 value |
| HashSet | 元素唯一 | hashCode() + equals() | 添加失败(返回 false) |
源码对比:
// HashMap put - 重复 key 会覆盖 value
public V put(K key, V value) {
// ...
if (e != null) { // key 已存在
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 覆盖旧值
return oldValue;
}
// ...
}
// HashSet add - 重复元素返回 false
public boolean add(E e) {
// map.put 返回 null 表示成功(原来没有)
// map.put 返回 PRESENT 表示原来已有
return map.put(e, PRESENT) == null;
}
示例代码:
// HashMap:重复 key 会覆盖
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
Integer old = map.put("A", 2); // 返回 1(被覆盖的值)
System.out.println(map.get("A")); // 输出 2
// HashSet:重复元素添加失败
HashSet<String> set = new HashSet<>();
boolean first = set.add("A"); // true
boolean second = set.add("A"); // false
System.out.println(set.size()); // 输出 1
2.3 区别三:null 值支持
| 类 | null key | null value | null 元素 |
|---|---|---|---|
| HashMap | ✅ 允许(1个) | ✅ 允许(多个) | N/A |
| HashSet | N/A | N/A | ✅ 允许(1个) |
// HashMap:支持 null key 和 null value
HashMap<String, String> map = new HashMap<>();
map.put(null, "nullKey"); // OK
map.put("key", null); // OK
map.put(null, "another"); // 覆盖之前的 null key
// HashSet:支持 null 元素(最多一个)
HashSet<String> set = new HashSet<>();
set.add(null); // OK,true
set.add(null); // false,已存在
2.4 区别四:实现的接口
| 类 | 实现的接口 | 所属体系 |
|---|---|---|
| HashMap | Map, Cloneable, Serializable | Map 体系 |
| HashSet | Set, Cloneable, Serializable | Collection 体系 |
2.5 区别五:初始化与容量
// HashMap 初始化
HashMap<String, Integer> map = new HashMap<>(); // 容量16,负载因子0.75
HashMap<String, Integer> map2 = new HashMap<>(32); // 指定初始容量
// HashSet 初始化(本质调用 HashMap)
HashSet<String> set = new HashSet<>(); // 底层 HashMap 容量16
HashSet<String> set2 = new HashSet<>(32); // 底层 HashMap 容量32
HashSet<String> set3 = new HashSet<>(32, 0.8f); // 指定容量和负载因子
HashSet 构造器源码:
// HashSet 构造器
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
2.6 区别六:添加/获取元素的方式
| 操作 | HashMap | HashSet |
|---|---|---|
| 添加 | put(key, value) | add(element) |
| 获取 | get(key) 返回 value | 无法直接获取单个元素 |
| 删除 | remove(key) 删除键值对 | remove(element) 删除元素 |
| 遍历 | entrySet() / keySet() | iterator() / 增强 for |
// HashMap:可以根据 key 获取 value
HashMap<String, Integer> map = new HashMap<>();
map.put("张三", 25);
Integer age = map.get("张三"); // 获取到 25
// HashSet:无法根据元素获取什么,只能判断是否存在
HashSet<String> set = new HashSet<>();
set.add("张三");
boolean exists = set.contains("张三"); // true
// 无法从 set 中“取出”张三,因为根本没有 value
2.7 区别七:遍历方式对比
// HashMap 遍历方式(3种)
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
// 方式1:遍历 Entry 集合
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
}
// 方式2:遍历 Key 集合
for (String key : map.keySet()) {
Integer value = map.get(key);
}
// 方式3:遍历 Value 集合
for (Integer value : map.values()) { }
// HashSet 遍历方式(2种)
HashSet<String> set = new HashSet<>();
set.add("A");
set.add("B");
// 方式1:增强 for
for (String element : set) {
System.out.println(element);
}
// 方式2:迭代器
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String element = it.next();
}
2.8 区别八:适用场景对比
| 场景 | 推荐 | 原因 |
|---|---|---|
| 存储键值对应关系 | HashMap | 天然的 K-V 结构 |
| 元素去重 | HashSet | 专为去重设计 |
| 快速根据 key 查找 value | HashMap | O(1) 时间复杂度 |
| 判断元素是否存在 | HashSet | contains() 方法 |
| 统计单词出现次数 | HashMap | 需要计数器的 value |
| 过滤重复数据 | HashSet | 自动去重 |
3. 完整对比总结表
| 对比维度 | HashMap | HashSet |
|---|---|---|
| 存储结构 | 键值对(Key-Value) | 单个元素 |
| 继承体系 | Map 体系 | Collection → Set 体系 |
| 底层实现 | 数组 + 链表 + 红黑树 | 基于 HashMap |
| 添加方法 | put(K,V) | add(E) |
| 获取方法 | get(K) 返回 V | 无单个获取方法 |
| 元素唯一性 | Key 唯一 | 元素唯一 |
| 重复处理 | 覆盖 value | 添加失败(返回 false) |
| null 支持 | null key(1个) + null value(多个) | null 元素(1个) |
| contains 方法 | containsKey() / containsValue() | contains() |
| 删除方法 | remove(key) | remove(element) |
| 遍历方式 | entrySet / keySet / values | 迭代器 / 增强 for |
| 典型应用 | 缓存、计数器、字典 | 去重、集合运算 |
4. 底层结构对比图
渲染错误: Mermaid 渲染失败: Lexical error on line 13. Unrecognized text. ...ubgraph HashSet 底层结构(就是HashMap) -----------------------^
5. 性能对比
| 操作 | HashMap | HashSet | 说明 |
|---|---|---|---|
| 添加 | O(1) 均摊 | O(1) 均摊 | HashSet 就是 HashMap.put |
| 删除 | O(1) 均摊 | O(1) 均摊 | HashSet 就是 HashMap.remove |
| 查找 | O(1) 均摊(按key) | O(1) 均摊(contains) | 本质相同 |
| 内存占用 | key + value + 节点 | key + PRESENT + 节点 | HashSet 多一个 PRESENT 对象开销 |
内存对比(存储 1000 个 String 元素):
// HashMap:1000 个键值对
Map<String, Integer> map = new HashMap<>(); // 每个 Entry 存储 K+V
// HashSet:1000 个元素(底层也是 1000 个键值对,value 固定)
Set<String> set = new HashSet<>(); // 每个 Entry 存储 K + PRESENT
HashSet 的内存占用略高于 HashMap(多了 PRESENT 对象的引用),但差异通常可忽略。
6. 相互转换
// HashSet → HashMap(将元素作为 key,value 自定义)
HashSet<String> set = new HashSet<>(Arrays.asList("A", "B", "C"));
HashMap<String, Integer> map = new HashMap<>();
for (String s : set) {
map.put(s, 0); // 每个元素初始计数为0
}
// HashMap → HashSet(提取 key)
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
HashSet<String> set = new HashSet<>(map.keySet()); // {A, B}
// 也可以提取 values
HashSet<Integer> valueSet = new HashSet<>(map.values()); // {1, 2}
7. 常见面试追问
Q1:既然 HashSet 底层是 HashMap,那为什么还要有 HashSet?
A:语义清晰 + 接口统一。
- 语义:有时只需要存储元素(Set),不需要键值对(Map)
- 接口:Set 继承 Collection,符合集合框架设计
- 限制:HashSet 屏蔽了 value 操作,避免误用
Q2:HashSet 的迭代顺序有保证吗?
A:没有。HashSet 不保证任何顺序,与 HashMap 的 key 遍历顺序一致(不确定)。
如果需要有序,使用:
LinkedHashSet:保持插入顺序TreeSet:保持排序顺序
Q3:HashSet 的 add 方法返回 false 时,元素被覆盖了吗?
A:没有被覆盖。因为 HashMap 的 key 已经存在,put 操作不会改变 key 对应的 value(但这里 value 是固定的 PRESENT),所以元素保持不变。
Q4:能用一个例子说明 HashMap 和 HashSet 的区别吗?
// 场景:统计单词出现次数
String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
// HashMap:统计次数
Map<String, Integer> countMap = new HashMap<>();
for (String word : words) {
countMap.put(word, countMap.getOrDefault(word, 0) + 1);
}
System.out.println(countMap); // {apple=3, banana=2, cherry=1}
// HashSet:去重(丢失计数信息)
Set<String> uniqueSet = new HashSet<>(Arrays.asList(words));
System.out.println(uniqueSet); // [apple, banana, cherry](顺序不确定)
8. 选型决策流程图
9. 代码示例:实际应用场景
9.1 场景一:缓存用户信息(HashMap)
public class UserCache {
private Map<String, User> cache = new HashMap<>();
public void put(String userId, User user) {
cache.put(userId, user);
}
public User get(String userId) {
return cache.get(userId); // O(1) 快速获取
}
}
9.2 场景二:过滤重复 IP(HashSet)
public class IpFilter {
private Set<String> blockedIps = new HashSet<>();
public void blockIp(String ip) {
blockedIps.add(ip);
}
public boolean isBlocked(String ip) {
return blockedIps.contains(ip); // O(1) 快速判断
}
}
9.3 场景三:求两个数组的交集
public Set<Integer> intersect(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
for (int num : nums1) set1.add(num);
Set<Integer> result = new HashSet<>();
for (int num : nums2) {
if (set1.contains(num)) {
result.add(num);
}
}
return result;
}
10. 一句话记忆
HashMap 存键值对,HashSet 只存 key;HashSet 底层是 HashMap,value 全是固定 PRESENT。
口诀:
HashMap 双列存,键值对一一对应;
HashSet 单列放,元素就是底层 key;
要问两者啥关系,HashSet 就是阉割版 HashMap。
如果你彻底搞懂了 HashSet 和 HashMap 的区别,欢迎点赞、收藏、转发!有任何疑问,评论区一起交流~

|
🌺The End🌺点点关注,收藏不迷路🌺
|
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_41840843/article/details/161387885




