2.Set集合
2.1Set集合概述和特点【应用】
-
不可以存储重复元素
-
没有索引,不能使用普通for循环遍历
补充说明:
-
Set的底层机制:
-
HashSet:基于哈希表(HashMap实现),通过对象的
hashCode()
方法+equals()
方法判断重复。 -
LinkedHashSet:HashSet的子类,特点是有序(按照存储顺序遍历),底层是哈希表+双向链表。
-
TreeSet:基于红黑树(自平衡二叉搜索树)实现,存储时自动排序。
-
-
注意:
-
HashSet 的“去重”依赖元素的
hashCode()
+equals()
方法。自定义对象必须重写这两个方法,否则无法去重。 -
TreeSet 的“去重”依赖元素的比较结果:
compareTo()
或比较器compare()
,如果返回0视为重复。
-
🧠 理论理解
Set 集合是 Java 集合框架中最核心的接口之一,属于单列集合体系,它的最大特性是元素唯一性。也就是说,在 Set 中存储的每个元素都是互不相同的。这是通过底层机制实现的,比如 HashSet 通过哈希表 + equals 方法保证元素不重复,TreeSet 则通过元素比较(自然排序或比较器排序)来去重。
此外,Set 没有索引,因此无法像 List 那样通过下标访问元素,也不能用普通的 for 循环来遍历。它只能通过迭代器(Iterator)或者增强 for 遍历。
总结:
-
HashSet 无序、去重、基于 HashMap;
-
LinkedHashSet 有序、去重、基于哈希 + 链表;
-
TreeSet 排序、去重、基于红黑树。
⚠️ 注意:如果向 Set 中存放自定义对象,要正确实现 hashCode
和 equals
方法(HashSet),或实现 Comparable
(TreeSet)。
🏢 企业实战理解
-
阿里巴巴:在用户唯一标识(如用户ID集合)处理时,利用 HashSet 快速去重,保证分布式消息系统中的幂等性。
-
字节跳动:在推荐算法中,使用 TreeSet 对候选内容排序,比如按热度 + 发布时间多条件排序,TreeSet 高效处理动态排名。
-
Google:分布式缓存一致性时,用 HashSet 存储已完成的同步任务 ID,避免重复计算。
-
OpenAI:在 ChatGPT 服务层防止重复请求时,内部短时缓存会用 Set 做唯一请求标识。
❓ 面试题:
什么是 Set 集合?它有哪些典型的实现类?各自的特点是什么?
✅ 参考答案:
Set 是 Java 集合框架中的一种单列集合,它的核心特点是:不允许存储重复元素。Set 集合不提供索引,因此不能通过下标访问元素。
常见实现类:
-
HashSet
-
底层:基于 HashMap 实现。
-
特点:元素无序、唯一;允许存放 null(最多一个)。
-
时间复杂度:增删查 O(1)。
-
-
LinkedHashSet
-
底层:HashMap + 双向链表。
-
特点:元素唯一且有插入顺序。
-
应用场景:既要去重又要保证遍历顺序。
-
-
TreeSet
-
底层:红黑树(TreeMap 实现)。
-
特点:元素唯一、自动排序(自然排序或比较器排序)。
-
时间复杂度:增删查 O(logN)。
-
大厂加分点:
-
**HashSet 去重机制:**依赖元素的
hashCode
+equals
方法; -
**TreeSet 去重机制:**依赖
compareTo
或Comparator
比较规则,返回 0 视为重复。
实战考点:
字节跳动曾问:“如果你的对象 hashCode 一致,但 equals 不一致,HashSet 是否会存两份?”
✅ 答案:不会。虽然 hashCode 相同,但 HashSet 还会进一步通过 equals 判断元素是否重复。
2.2Set集合的使用【应用】
存储字符串并遍历
public class MySet1 {
public static void main(String[] args) {
//创建集合对象
Set<String> set = new TreeSet<>();
//添加元素
set.add("ccc");
set.add("aaa");
set.add("aaa");
set.add("bbb");
// for (int i = 0; i < set.size(); i++) {
// //Set集合是没有索引的,所以不能使用通过索引获取元素的方法
// }
//遍历集合
Iterator<String> it = set.iterator();
while (it.hasNext()){
String s = it.next();
System.out.println(s);
}
System.out.println("-----------------------------------");
for (String s : set) {
System.out.println(s);
}
}
}
补充说明:
-
为什么使用
TreeSet
?
因为它自动排序输出,所以运行结果是:aaa bbb ccc
-
底层机制:
-
HashSet
内部其实是一个HashMap
,元素作为key,值是固定的Object类型常量。 -
TreeSet
内部是TreeMap
,通过比较器/自然排序实现。
-
🧠 理论理解
Set 的遍历方式跟 Collection 接口一致:迭代器(Iterator)和增强 for。因为 Set 没有索引,无法用下标访问。代码中我们用 TreeSet 示例了自动排序、去重功能。
常见应用:
-
检查元素是否唯一;
-
快速去重;
-
集合运算(并、交、差);
-
排序(TreeSet)。
底层机制:
-
HashSet:调用
add()
时,首先计算元素的哈希值(hashCode
),然后比较equals
,哈希值不同直接添加,哈希值相同再用equals
判断是否重复。 -
TreeSet:
add()
时通过compareTo
或compare
方法决定位置,如果比较结果为 0,视为重复不添加。
🏢 企业实战理解
-
腾讯云:安全风控模块中,使用 HashSet 存储已封禁 IP 列表,高效判断是否命中黑名单。
-
美团:大促秒杀活动场景,用 Set 存储下单用户的唯一标识,防止刷单。
-
亚马逊 AWS:S3 存储层中,用 HashSet 追踪数据块的唯一标识(block ID)去重,防止重复写入。
-
字节跳动:在直播服务中使用 TreeSet 排序直播间列表(比如观众数、点赞数排序)。
2️⃣ TreeSet 排序机制题
❓ 面试题:
TreeSet 是如何保证元素有序的?TreeSet 和 HashSet 在去重时的区别是什么?
✅ 参考答案:
-
TreeSet 排序机制:
TreeSet 内部基于红黑树(平衡二叉搜索树),插入元素时:
1️⃣ 如果构造函数没有指定比较器,则要求元素实现Comparable
接口,TreeSet 调用compareTo()
方法确定顺序;
2️⃣ 如果构造函数指定了比较器,则使用Comparator
的compare()
方法进行排序。 -
TreeSet 去重机制:
判断重复的依据是比较方法compareTo()
或compare()
的返回值:-
返回 0:视为重复,不存储;
-
非 0:视为不重复,存储。
-
-
HashSet 和 TreeSet 区别:
对比点 HashSet TreeSet 排序 不保证顺序 元素自动排序 去重机制 hashCode + equals compareTo 或 compare 返回值为 0 时去重 性能 O(1) 查找 O(logN) 查找 使用场景 快速去重、无需顺序 需要有序、按规则排序去重
场景题1:字节跳动——内容去重系统
题目描述:
字节跳动在搭建视频审核平台时,需要对每天上传的数百万条短视频内容进行标题去重。产品经理要求:
-
相同标题不能重复入库;
-
标题集合需支持频繁的查重验证;
-
数据规模巨大但对插入顺序不关心。
你作为Java开发负责人,推荐了使用Set
相关集合,请说明:
1️⃣ 为什么使用Set
而不是List
或Map
来做去重?
2️⃣ 你会优先选择HashSet
还是TreeSet
?为什么?
3️⃣ 如果后续产品提出“需要让标题按照字母顺序展示”的功能,该如何调整现有方案?
参考答案:
1️⃣ 为什么用Set:
Set
集合的本质就是“不可重复元素的集合”,其底层机制保证了元素唯一性(如HashSet
利用哈希表,TreeSet
利用红黑树+比较器)。而List
允许重复数据,Map
虽然键不重复,但本质是键值对结构,不适合仅做去重。因此,Set
天生适合做去重场景。
2️⃣ HashSet vs TreeSet:
-
HashSet:
-
优点:插入、删除、查找性能高,平均时间复杂度O(1),适合大数据量快速去重。
-
缺点:元素无序,展示时不满足排序需求。
-
-
TreeSet:
-
优点:自动排序(默认自然顺序或自定义比较器),支持范围查询。
-
缺点:插入、删除、查找时间复杂度O(logN)。
-
结合题目场景:“只要求去重,不要求顺序”,应优先使用HashSet
,性能更高效。
3️⃣ 需要字母排序:
此时应切换到TreeSet
并传入Comparator
,自定义按字母排序逻辑。比如:
Set<String> set = new TreeSet<>((s1, s2) -> s1.compareToIgnoreCase(s2));
这样既保证去重,又按字母顺序自动排序,满足新需求。
题目描述:
在阿里巴巴红包雨活动中,每个红包都有一个唯一的领取码。工程中需要:
-
实时判断领取码是否已存在;
-
活动期间发放的所有领取码需要保持唯一;
-
领取码是字符串形式,数量数百万条;
-
后续计划做排行榜功能,按领取码自然排序展示前100名。
请回答:
1️⃣ 如何设计领取码的存储集合?并说明理由。
2️⃣ 初期只是做唯一校验,后续又提出排行榜,是否需要调整?如何做得无缝切换?
参考答案:
1️⃣ 设计方案:
-
初期阶段只要求唯一校验,最佳方案是
HashSet<String>
。 -
理由:
-
HashSet
基于哈希表实现,查找/插入性能极高(O(1))。 -
领取码唯一性校验核心是“是否已存在”,无需关注顺序,
HashSet
完美匹配场景。
-
2️⃣ 后续排行榜展示:
-
排行榜需要“自然顺序”(领取码排序),此时
HashSet
无法满足,因为它不保证顺序。 -
建议:
-
✅ 方案一(兼容升级):初期阶段同时维护一个
TreeSet
副本,待排行榜功能上线时直接使用,代价是占用额外内存。 -
✅ 方案二(平滑过渡):初期只用
HashSet
,排行榜功能上线时,将HashSet
转为TreeSet
:Set<String> sortedSet = new TreeSet<>(hashSet);
-
这样在架构设计中,保持集合封装层的扩展性,留有切换空间即可。
3.TreeSet集合
3.1TreeSet集合概述和特点【应用】
-
不可以存储重复元素
-
没有索引
-
可以将元素按照规则进行排序
-
TreeSet():根据其元素的自然排序进行排序
-
TreeSet(Comparator comparator) :根据指定的比较器进行排序
-
🧠 理论理解
TreeSet 是一个可排序的 Set,它通过红黑树实现:
-
自动排序(自然排序 / 比较器排序);
-
元素唯一性(判断依据是 compareTo / compare 方法);
-
查找、插入、删除的时间复杂度 O(logN)。
注意点:
-
TreeSet 不允许存放 null(因为 null 无法比较);
-
插入的对象必须是可比较的,否则会抛出 ClassCastException。
🏢 企业实战理解
-
京东:在订单系统中,TreeSet 用来维护实时热销榜,按销量和下单时间动态排序。
-
字节跳动:在抖音短视频首页流中,TreeSet 快速排序内容流(结合热度分和发布时间)。
-
Google Cloud:云端对象存储按时间戳排序处理历史快照,TreeSet 高效完成插入排序。
-
OpenAI:在并发请求日志中,用 TreeSet 存储时间戳事件,保证顺序输出。
3️⃣ 实践应用题
❓ 面试题:
实际开发中如果需要存储自定义对象到 TreeSet,该对象必须满足什么条件?如果对象无法修改源码,该怎么排序?
✅ 参考答案:
✅ 方案 1:实现 Comparable 接口
对象类需要实现 Comparable
接口,重写 compareTo()
方法,规定排序规则(如年龄、姓名等)。
✅ 方案 2:使用比较器排序
如果对象类无法修改源码(如第三方类库),我们可以在创建 TreeSet 时传入 Comparator
比较器,通过 Lambda 表达式或匿名内部类实现排序逻辑。
示例:
TreeSet<MyObject> set = new TreeSet<>(
(o1, o2) -> o1.getAge() - o2.getAge()
);
大厂常问点:
-
如果既实现了
Comparable
,又传入了Comparator
,TreeSet 会优先哪个?
✅ 答案:优先 Comparator,只要指定了比较器,TreeSet 不再使用 Comparable。
3.2TreeSet集合基本使用【应用】
存储Integer类型的整数并遍历
public class TreeSetDemo01 {
public static void main(String[] args) {
//创建集合对象
TreeSet<Integer> ts = new TreeSet<Integer>();
//添加元素
ts.add(10);
ts.add(40);
ts.add(30);
ts.add(50);
ts.add(20);
ts.add(30);
//遍历集合
for(Integer i : ts) {
System.out.println(i);
}
}
}
补充:
-
底层机制: TreeSet 基于红黑树实现,特点:
-
查找/插入/删除时间复杂度为 O(logN)。
-
红黑树是一种自平衡二叉查找树,每次插入/删除后会自动进行旋转和重新着色来保持平衡。
-
🧠 理论理解
TreeSet 存储 Integer 类型时,使用 Integer 自带的自然排序(实现了 Comparable 接口)。因此,元素会按照从小到大自动排序。
实用场景:
-
排序去重整数列表;
-
数据分析时动态更新的有序数据。
🏢 企业实战理解
-
阿里云:动态获取 Redis Key 大小 TopN,用 TreeSet 维护实时排序。
-
滴滴出行:司机评分榜单,实时用 TreeSet 排序。
-
NVIDIA:GPU 租用系统中,实时统计 GPU 占用率并排序,TreeSet 维护。
3.3自然排序Comparable的使用【应用】
-
案例需求
-
存储学生对象并遍历,创建TreeSet集合使用无参构造方法
-
要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
-
-
实现步骤
-
使用空参构造创建TreeSet集合
-
用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
-
-
自定义的Student类实现Comparable接口
-
自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T o)方法
-
-
重写接口中的compareTo方法
-
重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
-
-
-
代码实现
学生类
public class Student implements Comparable<Student>{ private String name; private int age; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { //按照对象的年龄进行排序 //主要判断条件: 按照年龄从小到大排序 int result = this.age - o.age; //次要判断条件: 年龄相同时,按照姓名的字母顺序排序 result = result == 0 ? this.name.compareTo(o.getName()) : result; return result; } }
测试类
public class MyTreeSet2 { public static void main(String[] args) { //创建集合对象 TreeSet<Student> ts = new TreeSet<>(); //创建学生对象 Student s1 = new Student("zhangsan",28); Student s2 = new Student("lisi",27); Student s3 = new Student("wangwu",29); Student s4 = new Student("zhaoliu",28); Student s5 = new Student("qianqi",30); //把学生添加到集合 ts.add(s1); ts.add(s2); ts.add(s3); ts.add(s4); ts.add(s5); //遍历集合 for (Student student : ts) { System.out.println(student); } } }
🧠 理论理解
自然排序 = 对象本身定义的排序规则。实现方式:
1️⃣ 实现 Comparable<T>
接口;
2️⃣ 重写 compareTo(T o)
方法。
比较原则:
-
主要条件:年龄;
-
次要条件:姓名字母序。
这种做法让 TreeSet 自动识别排序逻辑。
🏢 企业实战理解
-
美团:骑手按接单数、评分排名,使用自然排序 TreeSet 自动维护。
-
腾讯视频:视频播放量榜单,用 TreeSet 按播放量自然排序。
-
Google Play 商店:App 排名按下载量、评分双条件排序,内部自定义 Comparable。
4️⃣ 深入原理题
❓ 面试题:
你知道 TreeSet 的底层数据结构是什么吗?TreeSet 如何保证插入时平衡?时间复杂度是多少?
✅ 参考答案:
TreeSet 底层基于TreeMap实现,核心数据结构是红黑树(Red-Black Tree),它是一种自平衡的二叉搜索树。
-
每次插入/删除都会触发红黑树的旋转和变色操作,保证整体平衡;
-
查找、插入、删除操作的时间复杂度均为 O(logN)。
红黑树的性质:
1️⃣ 根节点是黑色;
2️⃣ 不能有两个连续的红色节点;
3️⃣ 任意节点到其子孙叶子节点的路径上黑色节点数目相同。
大厂实战:
-
字节跳动:面试高频题会让你手画红黑树的结构变动过程,考察你对 TreeSet 插入平衡机制的理解。
3.4比较器排序Comparator的使用【应用】
-
案例需求
-
存储老师对象并遍历,创建TreeSet集合使用带参构造方法
-
要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
-
-
实现步骤
-
用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
-
比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法
-
重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
-
-
代码实现
老师类
public class Teacher { private String name; private int age; public Teacher() { } public Teacher(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Teacher{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
测试类
public class MyTreeSet4 { public static void main(String[] args) { //创建集合对象 TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() { @Override public int compare(Teacher o1, Teacher o2) { //o1表示现在要存入的那个元素 //o2表示已经存入到集合中的元素 //主要条件 int result = o1.getAge() - o2.getAge(); //次要条件 result = result == 0 ? o1.getName().compareTo(o2.getName()) : result; return result; } }); //创建老师对象 Teacher t1 = new Teacher("zhangsan",23); Teacher t2 = new Teacher("lisi",22); Teacher t3 = new Teacher("wangwu",24); Teacher t4 = new Teacher("zhaoliu",24); //把老师添加到集合 ts.add(t1); ts.add(t2); ts.add(t3); ts.add(t4); //遍历集合 for (Teacher teacher : ts) { System.out.println(teacher); } } }
🧠 理论理解
当类本身没有实现 Comparable,或排序规则不固定时,TreeSet 提供“比较器排序”:
-
构造时传入
Comparator
实现类或 Lambda; -
TreeSet 内部通过
compare()
方法判断顺序/重复。
优势:不修改类源码即可灵活排序。
🏢 企业实战理解
-
京东金融:信用卡优惠列表按多条件排序,TreeSet+比较器实现灵活多维度。
-
阿里达摩院:A/B 实验数据结果排序分析,Comparator 动态支持多维度。
-
英伟达:GPU 订单记录,TreeSet + 比较器按 GPU 型号 + 时间戳动态排序。
5️⃣ 易错题
❓ 面试题:
为什么 TreeSet 不允许存 null 元素?HashSet 为什么可以存 null?
✅ 参考答案:
-
TreeSet 不允许 null 元素:
因为 TreeSet 存储时需要进行比较(compareTo
或compare
),而 null 无法参与比较操作,否则会抛出 NullPointerException。 -
HashSet 允许 null 元素:
因为 HashSet 底层只依赖 hashCode 和 equals,而 null 在 Java 中是合法值,HashMap 中对 null 进行了特殊处理(null 的 hash 值固定为 0)。
大厂加分点:
-
TreeSet 存储 null 的行为在 JDK8 及以上是强制禁止的,底层会立即抛出 NPE。
3.5两种比较方式总结【理解】
-
两种比较方式小结
-
自然排序: 自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序
-
比较器排序: 创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序
-
在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序
-
-
两种方式中关于返回值的规则
-
如果返回值为负数,表示当前存入的元素是较小值,存左边
-
如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存
-
如果返回值为正数,表示当前存入的元素是较大值,存右边
-
🧠 理论理解
排序方式 | 实现原理 | 适用场景 |
---|---|---|
自然排序 | 元素类实现 Comparable ,重写 compareTo | 排序规则固定,且对象类可修改源码 |
比较器排序 | TreeSet 构造时传入 Comparator 实现类或 Lambda 实现 | 排序规则经常变化、类不能修改源码 |
核心点:
-
比较返回 0:视为重复不存;
-
返回负数:插入左子树;
-
返回正数:插入右子树。
🏢 企业实战理解
-
字节跳动:内容推荐排序,基础是自然排序,临时调整用比较器排序。
-
OpenAI:API 日志按时间戳自然排序,用户自定义查询时用比较器支持多条件查询。
-
Google Cloud:对象存储生命周期策略,按时间、容量多条件排序,比较器方案。
题目描述:
Google搜索引擎的日志系统中,记录了用户每天搜索的关键词。系统需要支持以下功能:
-
日志去重(同一天内重复的关键词只存一次);
-
对关键词进行字典序排序后提供搜索热度榜单;
-
日志体量超大,如何保证高性能和低内存开销?
作为系统架构师,请说明你在Java中怎么选型,如何兼顾性能和排序,遇到性能瓶颈时有哪些改进策略?
参考答案:
✅ 选型方案:
-
核心数据结构:
TreeSet<String>
。-
去重:TreeSet天然去重。
-
排序:TreeSet自动字典序排序。
-
性能:O(logN),可接受。
-
✅ 为什么不是HashSet:
-
HashSet
虽然性能更高(O(1)),但无法满足“字典序排序”需求。 -
项目目标包含“日志去重+字典序”,TreeSet是一次性满足的最优选。
✅ 性能瓶颈优化策略:
1️⃣ 分区+分治思想:
-
日志数据超大时,可根据关键词首字母或哈希值做“分片”,拆分成多份小集合,每份内部用TreeSet存储,实现分布式去重+排序。
2️⃣ 结合内存和磁盘(外排):
-
内存承压时,将TreeSet部分持久化到磁盘,采用归并排序方式合并多路结果,实现大规模排序。
3️⃣ 压缩与轻量优化:
-
使用
ConcurrentSkipListSet
作为并发版本的TreeSet; -
考虑关键词长度固定或模式明确时,引入Trie树(字典树)节省存储空间。