拓展阅读
todo
sdk 包的封装+文档
chat
详细介绍一下 HashSet
当然可以!我们来系统性地、通俗易懂地介绍一下 Java 中的 HashSet
。
✅ 一句话总结
HashSet
是一个基于HashMap
实现的 不允许元素重复、无序、查找超快 的集合。
它是集合框架中最常用的“去重神器”。
🧠 基本特性
特性 | 说明 |
---|---|
底层结构 | 基于 HashMap 实现,所有元素都作为 Map 的 key |
无序 | 元素插入顺序不保证(不像 LinkedHashSet ) |
元素唯一性 | 不允许重复元素,判断依据是:equals() + hashCode() |
允许 null 值 | 最多只能存一个 null 元素 |
非线程安全 | 多线程环境下需手动同步或用 ConcurrentHashMap |
🔧 底层原理简述
HashSet
本质上就是使用了一个 HashMap<K, Object>
来存储数据。每个插入的元素是 map.put(element, PRESENT)
。
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
- 所有元素都是作为
HashMap
的 key 来保存 value
统一使用一个哑对象PRESENT
(内容无关)
插入过程等价于:
HashSet<String> set = new HashSet<>();
set.add("hello");
// 等价于:
HashMap<String, Object> map = new HashMap<>();
map.put("hello", PRESENT);
🔢 常用方法
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 不会重复添加
set.contains("apple"); // true
set.remove("banana");
System.out.println(set); // [apple]
📌 元素为什么不能重复?
关键在于 HashMap
的 key 不能重复:
- 插入时调用元素的
hashCode()
计算哈希桶位置 - 如果有冲突,则使用
equals()
判断是否相等
也就是说:
你放进
HashSet
的类,一定要正确重写hashCode()
和equals()
方法,否则可能出现重复元素。
例子:
class Person {
String name;
int age;
// ❌ 如果不重写 equals/hashCode,HashSet 会当成两个不同对象
}
Set<Person> set = new HashSet<>();
set.add(new Person("Tom", 18));
set.add(new Person("Tom", 18)); // 会当成不同对象添加进去
✅ 正确方式:
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
🧮 时间复杂度分析
操作 | 复杂度(平均) | 说明 |
---|---|---|
add() | O(1) | 哈希定位+链表/树处理 |
remove() | O(1) | |
contains() | O(1) | |
遍历 | O(n) | 无序遍历 |
如果哈希函数设计不好 → 大量冲突 → 性能退化为 O(n)
🧪 扩容机制
和 HashMap
一样,HashSet
也有:
- 初始容量:默认 16
- 加载因子:默认 0.75
- 阈值:达到容量 × 加载因子就会扩容(变为原来的 2 倍)
- 扩容时元素会重新 hash 分配位置
🚩 和其他 Set 的区别
类型 | 底层实现 | 特点 |
---|---|---|
HashSet |
HashMap |
无序、允许 null |
LinkedHashSet |
LinkedHashMap |
有序(插入顺序) |
TreeSet |
红黑树(TreeMap ) |
自动排序,不允许 null |
CopyOnWriteArraySet |
Array | 线程安全、适合读多写少 |
🧠 面试常考知识点
问题 | 回答简要 |
---|---|
HashSet 是否线程安全? | 否。需要手动加锁或用 Collections.synchronizedSet() |
如何判断元素是否重复? | hashCode() + equals() |
HashSet 能存 null 吗? | 可以,但最多一个 null |
底层结构是什么? | 基于 HashMap ,元素是 key,value 是统一哑对象 |
如何遍历 HashSet? | 使用 for-each 、Iterator 、stream() 等方式 |
🎯 示例:正确使用 HashSet 存储自定义对象
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && Objects.equals(name, p.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Set<Person> set = new HashSet<>();
set.add(new Person("Tom", 18));
set.add(new Person("Tom", 18)); // 不会重复添加
📦 总结:HashSet 的优缺点
优点 | 缺点 |
---|---|
插入、查找快(O(1)) | 无序、不支持按序遍历 |
自动去重 | 元素必须实现 hashCode/equals |
内存消耗适中 | 多线程下需要额外处理同步 |
如果你还想了解:
HashSet
是如何扩容的?LinkedHashSet
/TreeSet
有什么使用场景?HashSet
的源码解析(如add()
方法底层)
我都可以继续详细讲下去 😄 你感兴趣哪个方向?