跳到主要内容 Java 集合框架进阶——Set 实现类深度解析与实战应用 | 极客日志
Java java 算法
Java 集合框架进阶——Set 实现类深度解析与实战应用 Java 集合框架中 Set 接口核心特性为元素不可重复。本文深入解析 HashSet、LinkedHashSet 和 TreeSet 三大实现类。HashSet 基于哈希表实现高效去重,时间复杂度 O(1)。LinkedHashSet 结合哈希表与双向链表,维护插入顺序。TreeSet 基于红黑树,支持自然排序或定制排序,提供导航方法。文章对比了三者底层结构、性能差异及适用场景,并通过电商商品标签管理系统实战案例,展示了如何根据需求选择合适实现类,解决去重、有序存储及统计排序问题。掌握 Set 集合原理有助于优化代码性能与数据结构选型。
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 接口基本一致,未新增专属方法,其核心差异体现在实现类的底层逻辑(去重、排序等)
public interface Set <E> extends Collection <E> {
boolean add (E e) ;
boolean addAll (Collection<? extends E> c) ;
boolean remove (Object o) ;
boolean contains (Object o) ;
Iterator<E> ;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
iterator
()
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 {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object ();
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 的关键。
3.2.1 add(E e) 方法:元素添加与去重机制 public boolean add (E e) {
return map.put(e, PRESENT) == null ;
}
当调用 add(E e) 方法时,HashSet 会将元素 e 作为 key 传入 HashMap 的 put 方法
HashMap 首先调用 e 的 hashCode() 方法计算哈希值,根据哈希值确定元素在数组中的桶位置
若桶位置为空,则直接创建节点存入,添加成功(返回 true)
若桶位置不为空(发生哈希冲突),则通过 equals() 方法比较桶中已有元素与 e:
若存在 equals() 返回 true 的元素,则说明 e 已存在,添加失败(返回 false)
若不存在,则将 e 加入桶中(链表或红黑树),添加成功(返回 true)
⚠️ 关键结论:HashSet 的去重依赖 hashCode() + equals() 方法的协同工作:
若两个元素的 hashCode() 返回值不同,HashSet 直接认为它们是不同元素,不会调用 equals()
若两个元素的 hashCode() 返回值相同,才会调用 equals() 进一步判断是否为同一元素
因此,若只重写 equals() 而不重写 hashCode(),会导致 hashCode() 不同但 equals() 相同的元素被重复添加(因为哈希值不同,存入不同桶,不会触发 equals() 比较)
3.2.2 contains(Object o) 方法:元素查找机制 public boolean contains (Object o) {
return map.containsKey(o);
}
HashMap 的 containsKey 方法查找流程:
计算 o 的 hashCode() 得到哈希值,确定桶位置
遍历桶中的元素(链表或红黑树),通过 equals() 方法比较是否存在匹配元素
找到则返回 true,否则返回 false
💡 哈希表的查找效率极高,理想情况下(无哈希冲突)时间复杂度为 O(1),最坏情况下(所有元素哈希冲突,链表结构)时间复杂度为 O(n),红黑树结构下为 O(log n)
3.2.3 remove(Object o) 方法:元素删除机制 public boolean remove (Object o) {
return map.remove(o) == PRESENT;
}
删除流程与查找流程类似:先通过 hashCode() 定位桶位置,再通过 equals() 找到目标元素,最后删除该元素(并维护链表/红黑树结构)。
3.3 哈希冲突与解决策略
3.3.1 哈希冲突的产生 当两个不同元素的 hashCode() 方法返回相同的哈希值时,它们会被分配到哈希表的同一个桶中,这种情况称为哈希冲突 。
class Person {
private String name;
private int age;
@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);
}
}
Person p1 = new Person ("张三" , 20 );
Person p2 = new Person ("张三" , 20 );
System.out.println(p1.equals(p2));
System.out.println(p1.hashCode() == p2.hashCode());
HashSet<Person> set = new HashSet <>();
set.add(p1);
set.add(p2);
System.out.println(set.size());
3.3.2 哈希冲突的解决策略(HashMap/HashSet 实现)
链地址法(拉链法) :将同一个桶中的冲突元素以链表形式存储,JDK 8 中当链表长度超过 8 且数组长度≥64 时,转为红黑树
扰动函数 :对 hashCode() 的返回值进行二次哈希计算(JDK 8 简化为一次异或和无符号右移),减少哈希冲突的概率
动态扩容 :当哈希表的负载因子(元素个数/数组容量)超过阈值(默认 0.75)时,数组容量扩容为原来的 2 倍,重新分配所有元素的桶位置
3.3.3 正确重写 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;
@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 () {
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());
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)
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 {
public LinkedHashSet () {
super (16 , .75f , true );
}
public LinkedHashSet (int initialCapacity) {
super (initialCapacity, .75f , true );
}
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 遍历需要遍历整个哈希表数组,包括空桶。
操作类型 HashSet LinkedHashSet 添加 100 万元素 89ms 102ms 查找单个元素 0ms 0ms 遍历 100 万元素 12ms 8ms
💡 结论:LinkedHashSet 的添加操作略慢于 HashSet(因为需要维护双向链表),但遍历效率更高;查找和删除效率与 HashSet 基本一致(均依赖哈希表)。
4.2.3 访问顺序模式(LinkedHashSet 扩展特性)
插入顺序(默认):遍历顺序与元素插入顺序一致
访问顺序:遍历顺序与元素最后访问时间一致(get() 或 put() 操作会将元素移到双向链表尾部)
虽然 LinkedHashSet 本身未直接提供访问顺序的构造函数,但可通过自定义 LinkedHashMap 实现:
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));
set.contains("B" );
System.out.println("访问 B 后遍历:" + new ArrayList <>(set));
}
}
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 {
private transient NavigableMap<E, Object> m;
private static final Object PRESENT = new Object ();
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() 方法进行排序,这就是自然排序。
基本类型包装类(Integer、Double、String 等):按数值/字典顺序排序
自定义类:需手动实现 Comparable 接口,重写 compareTo() 方法
class Student implements Comparable <Student> {
private String id;
private int score;
@Override
public int compareTo (Student o) {
if (this .score != o.score) {
return 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);
}
}
}
5.2.2 定制排序(自定义比较器) 若元素未实现 Comparable 接口,或需要自定义排序规则,可通过 TreeSet 的构造函数传入 Comparator 接口实现类,这就是定制排序。
class Employee {
private String name;
private int age;
}
public class TreeSetCustomSortTest {
public static void main (String[] args) {
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);
}
}
}
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 的核心是红黑树的插入、删除、查找操作。
5.3.1 add(E e) 方法:元素添加与红黑树插入 public boolean add (E e) {
return m.put(e, PRESENT) == null ;
}
若红黑树为空,创建根节点
若存在比较器,使用比较器的 compare() 方法查找插入位置;否则使用元素的 compareTo() 方法
若找到相同元素(比较结果为 0),则替换 value,返回旧 value(TreeSet 认为添加失败)
若未找到相同元素,创建新节点并插入红黑树
调整红黑树结构(变色、旋转),保证红黑树的平衡特性
插入节点 7 作为红色节点,发现父节点 6 也是红色,违反红黑树特性 4
进行变色操作:将祖父节点 5 变为红色,父节点 6 和叔父节点 4 变为黑色
若祖父节点是根节点,再将其变为黑色,调整完成
5.3.2 导航方法(NavigableSet 接口) TreeSet 实现了 NavigableSet 接口,提供了一系列强大的导航方法,用于查找元素的前驱、后继、范围查询等:
E lower (E e) ;
E floor (E e) ;
E ceiling (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 ));
System.out.println(set.floor(25 ));
System.out.println(set.ceiling(25 ));
System.out.println(set.higher(25 ));
System.out.println(set.pollFirst());
System.out.println(set.pollLast());
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:用户注册时存储用户名(需去重,无需有序)
Set<String> usernames = new HashSet <>();
usernames.add("zhangsan" );
usernames.add("lisi" );
usernames.add("zhangsan" );
System.out.println(usernames.size());
示例 2:记录用户操作日志(需去重,维护操作顺序)
Set<String> operationLogs = new LinkedHashSet <>();
operationLogs.add("用户登录" );
operationLogs.add("查询数据" );
operationLogs.add("修改数据" );
operationLogs.add("用户退出" );
for (String log : operationLogs) {
System.out.println(log);
}
示例 3:学生成绩排行榜(需去重,按分数降序排序,支持 TopN 查询)
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 ));
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;
private String productName;
private Set<String> tags;
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);
}
@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 {
private Map<String, Product> productMap = new HashMap <>();
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() {
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);
}
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 ();
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();
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();
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();
System.out.println("=== 4. 查询包含标签 '编程' 的商品 ===" );
List<Product> programmingProducts = tagManager.queryProductsByTag("编程" );
programmingProducts.forEach(System.out::println);
System.out.println();
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();
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 接口及其实现类。