Seal^_^头像
关注
Java 中 HashSet 和 HashMap 的核心区别:底层实现、存储结构与使用场景全解析封面图

Java 中 HashSet 和 HashMap 的核心区别:底层实现、存储结构与使用场景全解析


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

在 Java 集合框架中,HashSetHashMap 是使用频率最高的两个类。它们的名字相似,底层都依赖哈希表,但一个是单列集合(存储单个元素),一个是双列集合(存储键值对)。很多初学者容易混淆,面试中也是高频考点。

本文将深入剖析 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 = 元素数量

图解对比

HashSet底层

HashSet

实际上是

实际上是

实际上是

HashMap

Key1:Value1

Key2:Value2

Key3:Value3

元素A

元素B

元素C

'A': PRESENT

'B': PRESENT

'C': PRESENT

2.2 区别二:元素唯一性保证

唯一性约束判断依据重复处理
HashMapKey 唯一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 keynull valuenull 元素
HashMap✅ 允许(1个)✅ 允许(多个)N/A
HashSetN/AN/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 区别四:实现的接口

底层持有

«interface»

Map<K,V>

HashMap<K,V>

«interface»

Collection<E>

«interface»

Set<E>

HashSet<E>

实现的接口所属体系
HashMapMap, Cloneable, SerializableMap 体系
HashSetSet, Cloneable, SerializableCollection 体系

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 区别六:添加/获取元素的方式

操作HashMapHashSet
添加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 查找 valueHashMapO(1) 时间复杂度
判断元素是否存在HashSetcontains() 方法
统计单词出现次数HashMap需要计数器的 value
过滤重复数据HashSet自动去重

3. 完整对比总结表

对比维度HashMapHashSet
存储结构键值对(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. 性能对比

操作HashMapHashSet说明
添加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. 选型决策流程图


需要计数/统计

需要存储数据

存储结构是键值对?

HashMap
需要快速根据key查找value

只需要判断元素是否存在?

需要去重?

HashSet
自动去重

数据量大小?

ArrayList

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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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