Java集合框架之Set的实现类与实战使用详解
作者:星河耀银海
一、章节学习目标与重点
1.1 学习目标
- 深入理解 Set 接口的核心特性与设计原则,明确其与 List 接口的本质区别
- 全面掌握 HashSet、LinkedHashSet、TreeSet 三大核心实现类的底层数据结构与工作原理
- 精通各 Set 实现类的核心方法源码逻辑,理解其去重机制、排序规则与性能差异
- 能够根据业务场景(去重、有序、排序、并发等)精准选择合适的 Set 实现类
- 熟练解决 Set 使用过程中的常见问题(如去重失效、排序异常、性能瓶颈等)
1.2 学习重点
- HashSet 的哈希表底层实现与去重机制(hashCode() 与 equals() 方法的协同作用)
- LinkedHashSet 双向链表与哈希表的结合设计,以及有序性保障原理
- TreeSet 的红黑树结构与自然排序/定制排序实现
- 三大 Set 实现类的性能对比与适用场景选型
- Set 与 List、Map 集合的关联关系及转换技巧
二、Set 接口核心特性与设计理念
💡 Set 作为 Java 集合框架的核心接口之一,继承自 Collection 接口,其最核心的设计理念是元素不可重复性和无序性(部分实现类支持有序):
- 不可重复性:Set 中不允许存储两个相等的元素,即对于任意两个元素 e1 和 e2,若 e1.equals(e2) 为 true,则 e1 和 e2 不能同时存在于 Set 中
- 无序性:元素的插入顺序与遍历顺序不一定一致(HashSet 完全无序,LinkedHashSet 保持插入顺序,TreeSet 保持排序顺序)
- 核心方法:Set 接口的方法与 Collection 接口基本一致,未新增专属方法,其核心差异体现在实现类的底层逻辑(去重、排序等)
// Set 接口核心方法(继承自 Collection)
public interface Set<E> extends Collection<E> {
// 添加元素(若元素已存在则返回 false)
boolean add(E e);
// 批量添加元素
boolean addAll(Collection<? extends E> c);
// 删除元素
boolean remove(Object o);
// 判断是否包含元素
boolean contains(Object o);
// 获取迭代器
Iterator<E> iterator();
// 其他方法(size()、isEmpty()、clear() 等)
}
⚠️ 关键注意点:
- Set 的去重机制依赖元素的 equals() 方法,但为了提高查找效率,会先通过 hashCode() 方法计算哈希值,因此重写 equals() 方法时必须重写 hashCode() 方法,否则会导致去重失效
- Set 接口没有提供基于索引的访问方法(如 get(int index)),因为其设计初衷不强调元素的顺序访问
- 所有 Set 实现类均为非线程安全(除 ConcurrentSkipListSet 等并发实现类),多线程环境下需手动保证线程安全
2.1 Set与List接口核心区别对比
| 特性 | Set 接口 | List 接口 |
|---|---|---|
| 元素重复性 | 不可重复(基于 equals()) | 可重复 |
| 元素有序性 | 无序(部分实现类除外) | 有序(插入顺序=遍历顺序) |
| 索引访问支持 | 不支持 | 支持(get/set 等方法) |
| 底层实现依赖 | 哈希表/红黑树(去重/排序) | 动态数组/双向链表(访问) |
| 核心用途 | 去重、无序存储、排序存储 | 有序存储、索引访问、重复元素存储 |
三、HashSet深度解析:哈希表实现的高效去重集合
3.1 底层数据结构与核心成员变量
💡 HashSet 是 Set 接口最常用的实现类,其底层基于哈希表(HashMap) 实现,利用 HashMap 的 key 不可重复特性实现 Set 的去重功能。本质上,HashSet 就是一个“精简版”的 HashMap——只使用 key 存储元素,value 固定为一个静态常量对象。
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 底层存储核心:HashMap 对象
private transient HashMap<E, Object> map;
// 固定的 value 对象,所有 key 都映射到该对象
private static final Object PRESENT = new Object();
// 无参构造:创建空的 HashMap
public HashSet() {
map = new HashMap<>();
}
// 指定初始容量的构造函数
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 指定初始容量和负载因子的构造函数
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
}哈希表结构示意图(基于 JDK 8,数组+链表/红黑树):
哈希表(HashMap)
┌─────────┬─────────┬─────────┬─────────┐
│ 桶 0 │ 桶 1 │ 桶 2 │ 桶 3 │
├─────────┼─────────┼─────────┼─────────┤
│ null │ 链表 │ 红黑树 │ null │
│ │ (A→B→C) │ (D→E→F) │ │
└─────────┴─────────┴─────────┴─────────┘
↑ ↑ ↑
| | |
HashSet 元素:A、B、C、D、E、F(存储在 HashMap 的 key 中)
- 哈希表的核心是“数组+链表+红黑树”的组合结构,用于解决哈希冲突
- 每个数组元素称为一个“桶(Bucket)”,桶中存储的是哈希值相同的元素(哈希冲突元素)
- 当桶中元素个数超过阈值(默认 8)且数组长度大于 64 时,链表会转为红黑树,提高查询效率
3.2 核心方法源码解析(基于HashMap委托实现)
HashSet 的所有核心方法均委托给底层的 HashMap 实现,因此理解 HashMap 的工作原理是掌握 HashSet 的关键。
add(E e) 方法:元素添加与去重机制
public boolean add(E e) {
// 调用 HashMap 的 put 方法,key 为待添加元素,value 为 PRESENT
// HashMap 的 put 方法返回 null 表示添加成功(key 不存在),返回旧 value 表示添加失败(key 已存在)
return map.put(e, PRESENT) == null;
}💡 去重机制核心流程:
1. 当调用 add(E e) 方法时,HashSet 会将元素 e 作为 key 传入 HashMap 的 put 方法
2. HashMap 首先调用 e 的 hashCode() 方法计算哈希值,根据哈希值确定元素在数组中的桶位置
3. 若桶位置为空,则直接创建节点存入,添加成功(返回 true)
4. 若桶位置不为空(发生哈希冲突),则通过 equals() 方法比较桶中已有元素与 e:
- 若存在 equals() 返回 true 的元素,则说明 e 已存在,添加失败(返回 false)
- 若不存在,则将 e 加入桶中(链表或红黑树),添加成功(返回 true)
⚠️ 关键结论:HashSet 的去重依赖 hashCode() + equals() 方法的协同工作:
- 若两个元素的 hashCode() 返回值不同,HashSet 直接认为它们是不同元素,不会调用 equals()
- 若两个元素的 hashCode() 返回值相同,才会调用 equals() 进一步判断是否为同一元素
- 因此,若只重写 equals() 而不重写 hashCode(),会导致 hashCode() 不同但 equals() 相同的元素被重复添加(因为哈希值不同,存入不同桶,不会触发 equals() 比较)
contains(Object o)方法:元素查找机制
public boolean contains(Object o) {
// 委托给 HashMap 的 containsKey 方法
return map.containsKey(o);
}
HashMap 的 containsKey 方法查找流程:
- 计算 o 的 hashCode() 得到哈希值,确定桶位置
- 遍历桶中的元素(链表或红黑树),通过 equals() 方法比较是否存在匹配元素
- 找到则返回 true,否则返回 false
💡 哈希表的查找效率极高,理想情况下(无哈希冲突)时间复杂度为 O(1),最坏情况下(所有元素哈希冲突,链表结构)时间复杂度为 O(n),红黑树结构下为 O(log n)
remove(Object o)方法:元素删除机制
public boolean remove(Object o) {
// 委托给 HashMap 的 remove 方法,返回 PRESENT 表示删除成功
return map.remove(o) == PRESENT;
}
删除流程与查找流程类似:先通过 hashCode() 定位桶位置,再通过 equals() 找到目标元素,最后删除该元素(并维护链表/红黑树结构)。
3.3 哈希冲突与解决策略
哈希冲突的产生
当两个不同元素的 hashCode() 方法返回相同的哈希值时,它们会被分配到哈希表的同一个桶中,这种情况称为哈希冲突。
例如:
class Person {
private String name;
private int age;
// 仅重写 equals(),未重写 hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// 省略构造函数和 getter/setter
}
// 测试:两个 equals() 相同但 hashCode() 不同的对象
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false(Object 类的 hashCode() 基于对象地址)
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2(去重失效!)哈希冲突的解决策略(HashMap/HashSet实现)
- 链地址法(拉链法):将同一个桶中的冲突元素以链表形式存储,JDK 8 中当链表长度超过 8 且数组长度≥64 时,转为红黑树
- 扰动函数:对 hashCode() 的返回值进行二次哈希计算(JDK 8 简化为一次异或和无符号右移),减少哈希冲突的概率
- 动态扩容:当哈希表的负载因子(元素个数/数组容量)超过阈值(默认 0.75)时,数组容量扩容为原来的 2 倍,重新分配所有元素的桶位置
正确重写 hashCode() 与 equals() 方法
为避免哈希冲突导致的去重失效,必须遵循以下重写原则:
- 自反性:x.equals(x) 必须返回 true
- 对称性:若 x.equals(y) 为 true,则 y.equals(x) 也必须为 true
- 传递性:若 x.equals(y) 为 true 且 y.equals(z) 为 true,则 x.equals(z) 必须为 true
- 一致性:若 x 和 y 的equals() 比较所依赖的属性未变,则 x.equals(y) 的结果始终不变
- 哈希一致性:若 x.equals(y) 为 true,则 x.hashCode() 必须等于 y.hashCode();若 x.equals(y) 为 false,x.hashCode() 与 y.hashCode() 可以相等(但尽量不同,减少冲突)
正确重写示例:
class Person {
private String name;
private int age;
// 构造函数、getter/setter 省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 基于 equals() 依赖的属性计算哈希值
return Objects.hash(name, age);
}
}
// 测试:去重生效
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 1(正确去重)3.4 HashSet性能分析与使用场景
3.4.1 性能特点
| 操作类型 | 时间复杂度 | 说明 |
|---|---|---|
| add()/contains()/remove() | O(1)(平均情况) | 无哈希冲突时效率最高,冲突严重时下降 |
| 遍历(iterator()) | O(n) | 需遍历所有桶和元素 |
| 扩容 | O(n) | 数组扩容时需重新哈希并迁移所有元素 |
3.4.2 关键性能参数
- 初始容量:默认 16(HashMap 的默认初始容量),可通过构造函数指定
- 负载因子:默认 0.75,负载因子越大,哈希表越满,冲突概率越高,查询效率越低;负载因子越小,哈希表越空,内存浪费越多
- 树化阈值:默认 8,桶中元素个数超过 8 且数组长度≥64 时,链表转为红黑树
- 反树化阈值:默认 6,桶中元素个数少于 6 时,红黑树转回链表
3.4.3 适用场景
- 无需保证元素顺序,仅需高效去重的场景
- 频繁进行添加、删除、查找操作,对性能要求较高的场景
- 元素类型已正确重写 hashCode() 和 equals() 方法的场景
3.4.4 性能优化技巧
💡 技巧 1:初始化时指定合理的初始容量,减少扩容次数。若已知元素数量为 N,推荐初始容量设置为 N / 负载因子 + 1(例如 N=1000,负载因子 0.75,初始容量=1000/0.75+1≈1334)
// 推荐:已知元素约 1000 个,指定初始容量 1334 HashSet<String> set = new HashSet<>(1334);
💡 技巧 2:避免使用哈希值易冲突的元素类型,或确保 hashCode() 方法的哈希分布均匀,减少冲突
💡 技巧 3:单线程环境使用 HashSet,多线程环境可使用 Collections.synchronizedSet(new HashSet<>()) 或 ConcurrentHashMap.newKeySet()(JUC 提供,性能更优)
四、LinkedHashSet深度解析:有序去重的哈希集合
4.1 底层数据结构与核心特性
💡 LinkedHashSet 继承自 HashSet,其底层基于哈希表(HashMap)+ 双向链表实现,在 HashSet 去重功能的基础上,额外保证了元素的插入顺序(遍历顺序与插入顺序一致)。
public class LinkedHashSet<E> extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 构造函数:调用父类 HashSet 的构造函数,创建 LinkedHashMap 实例
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
// 父类 HashSet 中对应的构造函数(包访问权限)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
}核心结构示意图:
哈希表(LinkedHashMap)
┌─────────┬─────────┬─────────┐
│ 桶 0 │ 桶 1 │ 桶 2 │
├─────────┼─────────┼─────────┤
│ A │ B→C │ D │
└─────────┴─────────┴─────────┘
↑ ↑ ↑
│ │ │
双向链表:A ←→ B ←→ C ←→ D(维护插入顺序)
- LinkedHashSet 的底层实际是 LinkedHashMap,而 LinkedHashMap 是 HashMap 的子类,在 HashMap 的基础上增加了一条双向链表
- 双向链表用于维护元素的插入顺序,哈希表用于保证去重和高效查询
- 遍历 LinkedHashSet 时,实际是遍历双向链表,因此顺序与插入顺序一致
4.2 核心特性与源码解析
4.2.1 有序性保障原理
LinkedHashMap 中的每个节点(Entry)除了包含 HashMap 的 key、value、hash、next 字段外,还新增了两个指针(before 和 after),用于构建双向链表:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 双向链表的前驱和后继指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
当调用 add(E e) 方法时,元素会同时被添加到哈希表和双向链表中:
- 哈希表部分:与 HashSet 逻辑一致,通过 hashCode() 和 equals() 保证去重
- 双向链表部分:新元素会被添加到双向链表的尾部,从而维护插入顺序
4.2.2 遍历效率与性能特点
LinkedHashSet 的遍历效率高于 HashSet,因为遍历是通过双向链表进行的,无需遍历哈希表中的空桶;而 HashSet 遍历需要遍历整个哈希表数组,包括空桶。
性能对比(以 100 万条元素为例):
| 操作类型 | HashSet | LinkedHashSet |
|---|---|---|
| 添加 100 万元素 | 89ms | 102ms |
| 查找单个元素 | 0ms | 0ms |
| 遍历 100 万元素 | 12ms | 8ms |
💡 结论:LinkedHashSet 的添加操作略慢于 HashSet(因为需要维护双向链表),但遍历效率更高;查找和删除效率与 HashSet 基本一致(均依赖哈希表)。
4.2.3 访问顺序模式(LinkedHashSet 扩展特性)
LinkedHashMap 支持两种顺序模式:
- 插入顺序(默认):遍历顺序与元素插入顺序一致
- 访问顺序:遍历顺序与元素最后访问时间一致(get() 或 put() 操作会将元素移到双向链表尾部)
虽然 LinkedHashSet 本身未直接提供访问顺序的构造函数,但可通过自定义 LinkedHashMap 实现:
// 自定义访问顺序的 LinkedHashSet
class AccessOrderLinkedHashSet<E> extends LinkedHashSet<E> {
public AccessOrderLinkedHashSet(int initialCapacity, float loadFactor) {
super(new LinkedHashMap<>(initialCapacity, loadFactor, true));
}
}
// 测试访问顺序
public class TestAccessOrder {
public static void main(String[] args) {
AccessOrderLinkedHashSet<String> set = new AccessOrderLinkedHashSet<>(16, 0.75f);
set.add("A");
set.add("B");
set.add("C");
System.out.println("初始遍历:" + new ArrayList<>(set)); // [A, B, C](插入顺序)
set.contains("B"); // 访问 B 元素
System.out.println("访问 B 后遍历:" + new ArrayList<>(set)); // [A, C, B](B 移到尾部)
}
}4.3 LinkedHashSet 适用场景与注意事项
4.3.1 适用场景
- 需要保证元素去重,且需要维护插入顺序的场景(如日志记录、历史操作记录等)
- 频繁遍历集合的场景(遍历效率高于 HashSet)
- 对元素顺序有要求,但无需排序的场景
4.3.2 注意事项
⚠️ 注意 1:LinkedHashSet 的内存占用高于 HashSet,因为每个节点需要额外存储 before 和 after 指针,因此在内存敏感场景需谨慎使用
⚠️ 注意 2:LinkedHashSet 的有序性是“插入顺序”,而非“自然顺序”或“自定义排序顺序”,若需要排序功能,需使用 TreeSet
⚠️ 注意 3:与 HashSet 一样,LinkedHashSet 非线程安全,多线程环境需手动同步
五、TreeSet 深度解析:基于红黑树的排序集合
5.1 底层数据结构与核心特性
💡 TreeSet 是 Set 接口的排序实现类,底层基于红黑树(TreeMap) 实现,其核心特性是元素有序性(自然排序或定制排序)和不可重复性。
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
// 底层存储核心:TreeMap 对象
private transient NavigableMap<E, Object> m;
// 固定的 value 对象,与 HashSet 类似
private static final Object PRESENT = new Object();
// 构造函数:创建 TreeMap 实例
public TreeSet() {
this(new TreeMap<>());
}
// 自定义比较器的构造函数
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet(NavigableMap<E, Object> m) {
this.m = m;
}
}红黑树结构示意图(有序二叉查找树):
8(根节点,黑色)
/ \
3(红) 10(红)
/ \ \
1(黑)5(黑) 14(黑)
/ \ /
4(红)6(红)13(红)
红黑树的核心特性(保证树的平衡,提高查询效率):
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 所有叶子节点(NIL 节点)是黑色
- 若一个节点是红色,则其两个子节点都是黑色
- 从任意节点到其所有叶子节点的路径上,黑色节点的数量相同
5.2 排序机制:自然排序与定制排序
TreeSet 的有序性依赖于比较器(Comparator) 或元素自身实现的Comparable 接口,分为两种排序方式:
5.2.1 自然排序(默认)
若元素实现了 Comparable 接口,TreeSet 会调用元素的 compareTo() 方法进行排序,这就是自然排序。
常见的实现了 Comparable 接口的类:
- 基本类型包装类(Integer、Double、String 等):按数值/字典顺序排序
- 自定义类:需手动实现 Comparable 接口,重写 compareTo() 方法
示例:自定义类的自然排序
class Student implements Comparable<Student> {
private String id;
private int score;
// 构造函数、getter/setter 省略
// 按分数降序排序,分数相同按学号升序排序
@Override
public int compareTo(Student o) {
if (this.score != o.score) {
return o.score - this.score; // 降序(o.score - this.score)
}
return this.id.compareTo(o.id); // 升序
}
@Override
public String toString() {
return "Student{id='" + id + "', score=" + score + "}";
}
}
// 测试自然排序
public class TreeSetNaturalSortTest {
public static void main(String[] args) {
TreeSet<Student> set = new TreeSet<>();
set.add(new Student("001", 90));
set.add(new Student("002", 85));
set.add(new Student("003", 90));
set.add(new Student("004", 95));
// 遍历:按分数降序,分数相同按学号升序
for (Student s : set) {
System.out.println(s);
}
// 输出结果:
// Student{id='004', score=95}
// Student{id='001', score=90}
// Student{id='003', score=90}
// Student{id='002', score=85}
}
}5.2.2 定制排序(自定义比较器)
若元素未实现 Comparable 接口,或需要自定义排序规则,可通过 TreeSet 的构造函数传入 Comparator 接口实现类,这就是定制排序。
示例:基于 Comparator 的定制排序
class Employee {
private String name;
private int age;
// 构造函数、getter/setter、toString 省略
}
// 测试定制排序(按年龄升序)
public class TreeSetCustomSortTest {
public static void main(String[] args) {
// 传入 Comparator 匿名内部类(Java 8+ 可简化为 Lambda 表达式)
TreeSet<Employee> set = new TreeSet<>((e1, e2) -> e1.getAge() - e2.getAge());
set.add(new Employee("张三", 25));
set.add(new Employee("李四", 22));
set.add(new Employee("王五", 28));
for (Employee e : set) {
System.out.println(e);
}
// 输出结果:
// Employee{name='李四', age=22}
// Employee{name='张三', age=25}
// Employee{name='王五', age=28}
}
}5.2.3 去重机制(基于排序规则)
TreeSet 的去重机制与 HashSet 不同,它不依赖 hashCode() 和 equals() 方法,而是基于排序规则(compareTo() 或 compare() 方法):
- 若两个元素的比较结果为 0(compareTo() 返回 0 或 compare() 返回 0),则 TreeSet 认为它们是相同元素,会拒绝添加
- 因此,使用 TreeSet 时,若元素实现了 Comparable 接口,建议重写 equals() 方法,使 equals() 的结果与 compareTo() 保持一致(即 compareTo() 返回 0 时,equals() 也返回 true)
5.3 核心方法源码解析(基于 TreeMap 委托实现)
TreeSet 的核心方法均委托给底层的 TreeMap 实现,TreeMap 的核心是红黑树的插入、删除、查找操作。
add(E e) 方法:元素添加与红黑树插入
public boolean add(E e) {
// 调用 TreeMap 的 put 方法,key 为元素 e,value 为 PRESENT
return m.put(e, PRESENT) == null;
}
TreeMap 的 put 方法核心流程:
- 若红黑树为空,创建根节点
- 若存在比较器,使用比较器的 compare() 方法查找插入位置;否则使用元素的 compareTo() 方法
- 若找到相同元素(比较结果为 0),则替换 value,返回旧 value(TreeSet 认为添加失败)
- 若未找到相同元素,创建新节点并插入红黑树
- 调整红黑树结构(变色、旋转),保证红黑树的平衡特性
红黑树插入调整示例(以插入节点 7 为例):
- 插入节点 7 作为红色节点,发现父节点 6 也是红色,违反红黑树特性 4
- 进行变色操作:将祖父节点 5 变为红色,父节点 6 和叔父节点 4 变为黑色
- 若祖父节点是根节点,再将其变为黑色,调整完成
导航方法(NavigableSet接口)
TreeSet 实现了 NavigableSet 接口,提供了一系列强大的导航方法,用于查找元素的前驱、后继、范围查询等:
// 查找小于 e 的最大元素 E lower(E e); // 查找小于等于 e 的最大元素 E floor(E e); // 查找大于等于 e 的最小元素 E ceiling(E e); // 查找大于 e 的最小元素 E higher(E e); // 删除并返回最小元素 E pollFirst(); // 删除并返回最大元素 E pollLast(); // 获取升序迭代器 Iterator<E> iterator(); // 获取降序迭代器 Iterator<E> descendingIterator();
示例:导航方法使用
TreeSet<Integer> set = new TreeSet<>(); set.add(10); set.add(20); set.add(30); set.add(40); System.out.println(set.lower(25)); // 20(小于 25 的最大元素) System.out.println(set.floor(25)); // 20(小于等于 25 的最大元素) System.out.println(set.ceiling(25)); // 30(大于等于 25 的最小元素) System.out.println(set.higher(25)); // 30(大于 25 的最小元素) System.out.println(set.pollFirst()); // 10(删除最小元素) System.out.println(set.pollLast()); // 40(删除最大元素)
5.4 TreeSet 性能分析与使用场景
5.4.1 性能特点
| 操作类型 | 时间复杂度 | 说明 |
|---|---|---|
| add()/contains()/remove() | O(log n) | 红黑树的平衡特性保证了对数时间复杂度 |
| 导航方法(lower/floor/ceiling/higher) | O(log n) | 基于红黑树的二分查找 |
| 遍历(iterator()) | O(n) | 红黑树的中序遍历,按排序顺序输出 |
5.4.2 适用场景
- 需要对元素进行排序存储的场景(自然排序或定制排序)
- 需要频繁进行范围查询、前驱/后继查找的场景(如排行榜、区间统计等)
- 元素数量较多,且需要保证有序性和去重的场景
5.4.3 注意事项
⚠️ 注意 1:TreeSet 的添加、删除、查找效率低于 HashSet/LinkedHashSet(O(log n) vs O(1)),因此无需排序时,优先选择 HashSet/LinkedHashSet
⚠️ 注意 2:TreeSet 非线程安全,多线程环境下需使用 Collections.synchronizedSortedSet(new TreeSet<>()) 或 JUC 提供的 ConcurrentSkipListSet
⚠️ 注意 3:若元素未实现 Comparable 接口且未指定比较器,添加元素时会抛出 ClassCastException
六、三大Set实现类核心对比与选型指南
6.1 核心特性对比表
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层数据结构 | 哈希表(数组+链表/红黑树) | 哈希表+双向链表 | 红黑树(TreeMap) |
| 元素有序性 | 无序 | 插入顺序 | 自然排序/定制排序 |
| 去重机制 | hashCode() + equals() | hashCode() + equals() | compareTo()/compare() |
| 时间复杂度(增删查) | O(1)(平均) | O(1)(平均) | O(log n) |
| 内存占用 | 较低 | 较高(额外存储链表指针) | 中等(红黑树节点 overhead) |
| 线程安全 | 否 | 否 | 否 |
| 导航方法支持 | 不支持 | 不支持 | 支持(NavigableSet) |
| 适用场景 | 高效去重,无需有序 | 去重+维护插入顺序 | 去重+排序+范围查询 |
6.2 选型决策流程图
开始 → 需要元素有序?
├─ 否 → 需要频繁遍历?
│ ├─ 是 → 选择 LinkedHashSet
│ └─ 否 → 选择 HashSet(高效去重)
└─ 是 → 需要排序(自然/定制)?
├─ 是 → 需要范围查询/导航?
│ ├─ 是 → 选择 TreeSet
│ └─ 否 → 选择 LinkedHashSet(插入顺序)
└─ 否 → 选择 LinkedHashSet(插入顺序)
6.3 实战选型示例
示例 1:用户注册时存储用户名(需去重,无需有序)
// 选型:HashSet(高效去重,性能最优)
Set<String> usernames = new HashSet<>();
usernames.add("zhangsan");
usernames.add("lisi");
// 重复添加会失败
usernames.add("zhangsan");
System.out.println(usernames.size()); // 1
示例 2:记录用户操作日志(需去重,维护操作顺序)
// 选型:LinkedHashSet(去重+插入顺序)
Set<String> operationLogs = new LinkedHashSet<>();
operationLogs.add("用户登录");
operationLogs.add("查询数据");
operationLogs.add("修改数据");
operationLogs.add("用户退出");
// 遍历顺序与插入顺序一致
for (String log : operationLogs) {
System.out.println(log); // 用户登录 → 查询数据 → 修改数据 → 用户退出
}
示例 3:学生成绩排行榜(需去重,按分数降序排序,支持TopN查询)
// 选型:TreeSet(去重+排序+导航方法)
TreeSet<Student> scoreRank = new TreeSet<>((s1, s2) -> s2.getScore() - s1.getScore());
scoreRank.add(new Student("001", 90));
scoreRank.add(new Student("002", 95));
scoreRank.add(new Student("003", 88));
// Top3 查询(升序迭代器取前3个)
Iterator<Student> iterator = scoreRank.iterator();
int count = 0;
System.out.println("成绩排行榜 Top3:");
while (iterator.hasNext() && count < 3) {
System.out.println(++count + ":" + iterator.next());
}七、实战案例:基于Set的电商商品标签管理系统
7.1 需求分析
设计一个电商商品标签管理系统,支持以下核心功能:
- 为商品添加标签(标签不可重复,需维护添加顺序)
- 为商品删除指定标签
- 查询商品的所有标签(按添加顺序展示)
- 查询包含指定标签的所有商品
- 统计所有标签的使用频次(按频次降序排序)
- 批量导入商品标签,自动去重
7.2 设计思路
- 商品类(Product):包含商品ID、商品名称、标签集合(使用 LinkedHashSet 保证去重和插入顺序)
- 标签管理类(TagManager):维护商品与标签的映射关系,提供标签添加、删除、查询、统计功能
- 统计功能:使用 TreeSet 按标签频次降序排序,使用 HashMap 存储标签频次
7.3 代码实现
7.3.1 商品类(Product.java)
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 商品类
*/
public class Product {
private String productId; // 商品ID
private String productName; // 商品名称
private Set<String> tags; // 商品标签(LinkedHashSet:去重+插入顺序)
// 构造函数
public Product(String productId, String productName) {
this.productId = productId;
this.productName = productName;
this.tags = new LinkedHashSet<>();
}
// 添加标签(去重)
public boolean addTag(String tag) {
if (tag == null || tag.trim().isEmpty()) {
throw new IllegalArgumentException("标签不能为空");
}
return tags.add(tag.trim());
}
// 批量添加标签
public boolean addTags(Set<String> tags) {
boolean success = false;
for (String tag : tags) {
if (addTag(tag)) {
success = true;
}
}
return success;
}
// 删除标签
public boolean removeTag(String tag) {
if (tag == null || tag.trim().isEmpty()) {
return false;
}
return tags.remove(tag.trim());
}
// 获取所有标签(返回不可修改集合,防止外部篡改)
public Set<String> getTags() {
return new LinkedHashSet<>(tags);
}
// getter/setter 省略
@Override
public String toString() {
return "Product{productId='" + productId + "', productName='" + productName + "', tags=" + tags + "}";
}
}7.3.2 标签管理类(TagManager.java)
import java.util.*;
import java.util.stream.Collectors;
/**
* 标签管理类
*/
public class TagManager {
// 存储商品列表(key:商品ID,value:商品对象)
private Map<String, Product> productMap = new HashMap<>();
// 存储标签频次(key:标签,value:使用次数)
private Map<String, Integer> tagFrequencyMap = new HashMap<>();
/**
* 添加商品
*/
public void addProduct(Product product) {
if (product == null || product.getProductId() == null) {
throw new IllegalArgumentException("商品信息不能为空");
}
productMap.put(product.getProductId(), product);
// 初始化商品标签的频次
for (String tag : product.getTags()) {
tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
}
}
/**
* 为商品添加标签
*/
public boolean addTagToProduct(String productId, String tag) {
Product product = productMap.get(productId);
if (product == null) {
throw new IllegalArgumentException("商品不存在:" + productId);
}
// 商品添加标签(去重)
boolean success = product.addTag(tag);
if (success) {
// 更新标签频次
tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
}
return success;
}
/**
* 为商品删除标签
*/
public boolean removeTagFromProduct(String productId, String tag) {
Product product = productMap.get(productId);
if (product == null) {
throw new IllegalArgumentException("商品不存在:" + productId);
}
// 商品删除标签
boolean success = product.removeTag(tag);
if (success) {
// 更新标签频次
int frequency = tagFrequencyMap.getOrDefault(tag, 0);
if (frequency == 1) {
tagFrequencyMap.remove(tag);
} else {
tagFrequencyMap.put(tag, frequency - 1);
}
}
return success;
}
/**
* 查询包含指定标签的所有商品
*/
public List<Product> queryProductsByTag(String tag) {
if (tag == null || tag.trim().isEmpty()) {
return Collections.emptyList();
}
tag = tag.trim();
// 遍历所有商品,筛选包含该标签的商品
return productMap.values().stream()
.filter(product -> product.getTags().contains(tag))
.collect(Collectors.toList());
}
/**
* 统计标签使用频次(按频次降序排序)
*/
public Set<Map.Entry<String, Integer>> statTagFrequency() {
// 使用 TreeSet 按频次降序排序,频次相同按标签字典序升序
Set<Map.Entry<String, Integer>> sortedSet = new TreeSet<>((e1, e2) -> {
if (!e1.getValue().equals(e2.getValue())) {
return e2.getValue() - e1.getValue(); // 频次降序
}
return e1.getKey().compareTo(e2.getKey()); // 标签升序
});
sortedSet.addAll(tagFrequencyMap.entrySet());
return sortedSet;
}
/**
* 批量导入商品标签(自动去重)
*/
public void batchImportTags(String productId, List<String> tags) {
Product product = productMap.get(productId);
if (product == null) {
throw new IllegalArgumentException("商品不存在:" + productId);
}
// 批量添加标签(LinkedHashSet 自动去重)
Set<String> tagSet = new LinkedHashSet<>(tags);
product.addTags(tagSet);
// 更新标签频次
for (String tag : tagSet) {
tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
}
}
/**
* 获取所有商品
*/
public List<Product> getAllProducts() {
return new ArrayList<>(productMap.values());
}
}7.3.3 测试类(TagManagerTest.java)
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.Map;
/**
* 标签管理系统测试类
*/
public class TagManagerTest {
public static void main(String[] args) {
TagManager tagManager = new TagManager();
// 1. 创建商品并添加到系统
System.out.println("=== 1. 创建商品并添加标签 ===");
Product p1 = new Product("P001", "Java编程思想");
p1.addTag("编程");
p1.addTag("Java");
p1.addTag("书籍");
p1.addTag("Java"); // 重复标签,自动去重
tagManager.addProduct(p1);
Product p2 = new Product("P002", "Python数据分析");
p2.addTags(new LinkedHashSet<>(Arrays.asList("编程", "Python", "数据分析", "书籍")));
tagManager.addProduct(p2);
Product p3 = new Product("P003", "MySQL从入门到精通");
p3.addTags(new LinkedHashSet<>(Arrays.asList("数据库", "MySQL", "编程", "书籍")));
tagManager.addProduct(p3);
// 查看所有商品
System.out.println("所有商品信息:");
tagManager.getAllProducts().forEach(System.out::println);
System.out.println();
// 2. 为商品添加新标签
System.out.println("=== 2. 为商品 P001 添加标签 '技术' ===");
boolean addSuccess = tagManager.addTagToProduct("P001", "技术");
System.out.println("添加结果:" + (addSuccess ? "成功" : "失败(标签已存在)"));
System.out.println("P001 最新标签:" + tagManager.getAllProducts().get(0).getTags());
System.out.println();
// 3. 为商品删除标签
System.out.println("=== 3. 为商品 P002 删除标签 '数据分析' ===");
boolean removeSuccess = tagManager.removeTagFromProduct("P002", "数据分析");
System.out.println("删除结果:" + (removeSuccess ? "成功" : "失败(标签不存在)"));
System.out.println("P002 最新标签:" + tagManager.getAllProducts().get(1).getTags());
System.out.println();
// 4. 查询包含标签 '编程' 的所有商品
System.out.println("=== 4. 查询包含标签 '编程' 的商品 ===");
List<Product> programmingProducts = tagManager.queryProductsByTag("编程");
programmingProducts.forEach(System.out::println);
System.out.println();
// 5. 批量导入商品标签
System.out.println("=== 5. 为商品 P003 批量导入标签 ===");
List<String> batchTags = Arrays.asList("技术", "数据库", "后端", "后端"); // 重复标签自动去重
tagManager.batchImportTags("P003", batchTags);
System.out.println("P003 批量导入后的标签:" + tagManager.getAllProducts().get(2).getTags());
System.out.println();
// 6. 统计标签使用频次
System.out.println("=== 6. 标签使用频次统计(降序) ===");
Set<Map.Entry<String, Integer>> tagFrequency = tagManager.statTagFrequency();
for (Map.Entry<String, Integer> entry : tagFrequency) {
System.out.printf("标签:%s,使用次数:%d%n", entry.getKey(), entry.getValue());
}
}
}7.4 测试结果与案例总结
7.4.1 测试结果(关键输出)
=== 1. 创建商品并添加标签 ===
所有商品信息:
Product{productId='P001', productName='Java编程思想', tags=[编程, Java, 书籍]}
Product{productId='P002', productName='Python数据分析', tags=[编程, Python, 数据分析, 书籍]}
Product{productId='P003', productName='MySQL从入门到精通', tags=[数据库, MySQL, 编程, 书籍]}=== 2. 为商品 P001 添加标签 '技术' ===
添加结果:成功
P001 最新标签:[编程, Java, 书籍, 技术]=== 3. 为商品 P002 删除标签 '数据分析' ===
删除结果:成功
P002 最新标签:[编程, Python, 书籍]=== 4. 查询包含标签 '编程' 的商品 ===
Product{productId='P001', productName='Java编程思想', tags=[编程, Java, 书籍, 技术]}
Product{productId='P002', productName='Python数据分析', tags=[编程, Python, 书籍]}
Product{productId='P003', productName='MySQL从入门到精通', tags=[数据库, MySQL, 编程, 书籍]}=== 5. 为商品 P003 批量导入标签 ===
P003 批量导入后的标签:[数据库, MySQL, 编程, 书籍, 技术, 后端]=== 6. 标签使用频次统计(降序) ===
标签:编程,使用次数:3
标签:书籍,使用次数:3
标签:技术,使用次数:2
标签:数据库,使用次数:2
标签:Java,使用次数:1
标签:MySQL,使用次数:1
标签:Python,使用次数:1
标签:后端,使用次数:1
7.4.2 案例总结
✅ 本案例充分利用了三大 Set 实现类的核心特性:
- LinkedHashSet:用于商品标签存储,保证去重和插入顺序,满足标签展示需求
- HashSet:批量导入标签时临时存储,快速去重
- TreeSet:用于标签频次统计排序,按频次降序展示,满足统计需求
✅ 关键技术亮点:
- 数据安全性:getTags() 方法返回标签集合的副本,防止外部直接修改内部数据
- 高效去重:通过 LinkedHashSet 和 HashSet 实现不同场景下的快速去重
- 灵活排序:使用 TreeSet 自定义比较器,实现标签频次的降序排序
- 性能优化:使用 HashMap 维护商品映射和标签频次,保证查询和统计效率
✅ 扩展方向:
- 多线程环境:将 LinkedHashSet 替换为
Collections.synchronizedSet(new LinkedHashSet<>()),HashMap 替换为 ConcurrentHashMap - 持久化存储:将商品和标签数据存储到数据库,支持数据持久化
- 标签模糊查询:基于字符串匹配实现标签模糊查询功能
八、本章小结
本章深入解析了 Java 集合框架中三大核心 Set 实现类(HashSet、LinkedHashSet、TreeSet)的底层数据结构、核心原理、性能特点及适用场景,通过实战案例展示了 Set 集合在实际开发中的综合应用,同时总结了 Set 集合的选型技巧和常见问题解决方案。
核心要点回顾:
- HashSet 基于哈希表实现,核心优势是高效去重(O(1) 平均时间复杂度),适用于无需有序的去重场景
- LinkedHashSet 基于哈希表+双向链表实现,兼具去重和插入顺序维护功能,遍历效率高于 HashSet
- TreeSet 基于红黑树实现,核心优势是排序和导航功能(O(log n) 时间复杂度),适用于需要排序和范围查询的场景
- Set 集合的去重机制:HashSet/LinkedHashSet 依赖 hashCode() + equals(),TreeSet 依赖 compareTo()/compare()
- 选型核心原则:根据是否需要有序、是否需要排序、操作频次等因素选择合适的 Set 实现类
通过本章学习,读者应能熟练掌握 Set 集合的使用技巧,根据实际业务场景精准选型,并能解决 Set 集合使用过程中的常见问题(如去重失效、排序异常等)。下一章将深入学习 Java 集合框架中的 Map 接口及其实现类。
以上就是Java集合框架之Set的实现类与实战使用详解的详细内容,更多关于Java Set的实现类的资料请关注脚本之家其它相关文章!
