Java Set集合终极指南:去重+排序底层原理+实战避坑,面试开发双通关!
👉 公众号:咖啡Java研习室(回复【学习资料】领取福利)
宝子们是不是被重复数据搞得头大?——存用户ID不小心重复录入,统计数据出错;收集关键词标签时,相同标签堆一堆,杂乱无章;抽奖活动怕用户重复中奖,还要手动判重… 这些糟心问题,Set集合能一键解决!作为Java集合框架的“去重专用工具”,Set的核心价值就是“自动过滤重复元素”,今天这篇干货从“底层原理→实战用法→面试考点”全讲透,附可直接运行的标签管理系统案例,新手能抄作业,进阶选手能查漏补缺~ 文末有专属福利,记得看到最后!
| 划重点 | Set的核心是“无序、不可重复”,HashSet和TreeSet分工明确!要快速去重选HashSet,要去重+排序选TreeSet,记住这个原则,开发不踩坑,面试直接秒答~ |
|---|
一、先搞懂:Set到底是什么?(核心特点+应用场景)
Set是Collection接口的核心分支,专门存储“无序、不可重复”的单个数据,和List形成鲜明对比,两个核心特点直击“重复数据”痛点:
- 不可重复性:Set最核心的价值——集合中不会有两个相等的元素。比如存两次“用户ID=1001”,Set只会保留一个,自动过滤重复值,不用手动判断;
- 无序性:不保证元素的插入顺序与遍历顺序一致(和List的“有序”完全相反)。注意:TreeSet是例外,它会按元素大小排序,遍历顺序固定。
👉 关键区别:Set没有索引!这是它和List最直观的不同,不能通过下标获取元素,遍历只能用增强for或迭代器,新手一定要记牢。
典型应用场景:存储用户唯一ID、关键词标签去重、系统配置不重复参数、抽奖活动中奖用户集合(避免重复中奖)、日志去重统计。
二、核心拆解:Set两大实现类(底层+对比+面试考点)
Set是接口不能直接new,实际开发中用两个“主力”实现类:HashSet(去重效率之王)和TreeSet(去重+排序一体化)。两者底层结构完全不同,适用场景天差地别,搞懂底层才能选对工具。
1. HashSet:基于哈希表,快速去重的“天花板”
日常开发80%的去重场景都用HashSet,核心优势是“去重快、增删查效率高”,根源在它的底层结构——哈希表。
① 底层原理:哈希表的“去重魔法”
JDK8+中,HashSet底层是“数组+链表+红黑树”的组合结构,既保证去重又兼顾效率,核心逻辑拆成3个关键点:
- 哈希值定位:元素存入时,先调用
hashCode()方法得到哈希值,再通过哈希值计算数组存储位置(下标); - 哈希冲突解决:若两个元素哈希值对应下标相同(哈希冲突),用链表串起来;链表长度超过8时,自动转红黑树(提升查询效率);
- 去重判断逻辑:先通过哈希值找存储位置,再用
equals()方法验证元素是否相等——哈希值不同则一定不相等;哈希值相同必须用equals确认,避免“哈希碰撞”导致去重失效。
👉 通俗比喻:哈希表像“按身份证分区的小区”,哈希值是“身份证号”,数组下标是“楼栋号”;哈希冲突是“同一楼栋住多人”,用链表/红黑树记录;去重就是“检查同一楼栋有没有身份证号完全相同的人”。
② 核心优缺点&适用场景
| 维度 | 具体说明 |
|---|---|
| 核心优点 | 增删查效率高,时间复杂度接近O(1);去重速度快,适合大数据量场景; |
| 核心缺点 | 无序(不保证插入顺序);存自定义对象必须重写hashCode()和equals(),否则去重失效; |
| 适用场景 | 只需去重、不关心顺序:用户ID集合、关键词标签去重、日志去重统计、缓存不重复数据; |
| 面试考点 | 底层数据结构(数组+链表+红黑树)、去重原理(hashCode+equals)、哈希冲突解决方式; |
2. TreeSet:基于红黑树,去重+排序“二合一”
如果需要“去重+排序”(比如学生成绩排名、商品价格排序),HashSet就不够用了,TreeSet是一体化解决方案——自动去重同时,按规则排序。
① 底层原理:红黑树的“排序魔法”
TreeSet底层是红黑树(自平衡二叉排序树),核心特点是“自动按元素大小排序”,排序规则分两种:
- 自然排序:元素实现
Comparable接口,重写compareTo()方法(比如Integer按数值、String按字典序); - 定制排序:创建TreeSet时传入
Comparator接口实现类,自定义排序规则(比如学生按年龄倒序、商品按销量排序)。
👉 去重逻辑:通过排序规则判断元素是否“相等”——compareTo()返回0(或compare()返回0),则认为重复,自动过滤。
② 核心优缺点&适用场景
| 维度 | 具体说明 |
|---|---|
| 核心优点 | 自动去重+自动排序(自然排序/定制排序);遍历顺序固定,适合有序展示; |
| 核心缺点 | 增删查效率比HashSet低,时间复杂度O(log n);排序规则必须明确,否则存自定义对象报错; |
| 适用场景 | 去重+排序:学生成绩排名、商品价格从低到高展示、按时间排序的日志集合、排行榜功能; |
| 面试考点 | 底层红黑树特点、两种排序方式(Comparable vs Comparator)、去重与排序的关联; |
新手必记:Set实现类选择口诀
- 仅去重,不关心顺序 → HashSet(效率优先);
- 去重+排序 → TreeSet(功能优先);
- 去重+保留插入顺序 → LinkedHashSet(HashSet子类,链表记录插入顺序,兼顾效率和顺序);
三、实战必备:Set通用API(新手直接抄作业)
Set的API和Collection接口完全一致,无额外扩展方法,核心是“增删查”,但没有索引相关方法(比如get()、set())。下面以HashSet为例,讲常用API,代码可直接复制运行!
1. 核心API:增删查基础操作
import java.util.HashSet;
import java.util.Set;
public class SetApiDemo {
public static void main(String[] args) {
// 1. 创建Set(泛型指定元素类型,避免类型转换错误)
Set<String> cities = new HashSet<>();
// 2. 增:add()(重复元素返回false,不添加)
boolean add1 = cities.add("北京");
boolean add2 = cities.add("上海");
boolean add3 = cities.add("北京"); // 重复元素,添加失败
System.out.println("添加北京成功?" + add1); // 输出true
System.out.println("重复添加北京成功?" + add3); // 输出false
System.out.println("当前集合:" + cities); // 输出[北京, 上海](顺序可能不同)
// 3. 查:contains()、size()、isEmpty()
boolean hasShanghai = cities.contains("上海"); // 判断是否包含元素
int size = cities.size(); // 获取元素个数
boolean isEmpty = cities.isEmpty(); // 判断是否为空
System.out.println("包含上海?" + hasShanghai + ",元素个数:" + size + ",是否为空?" + isEmpty);
// 4. 删:remove()、clear()
boolean remove = cities.remove("北京"); // 按元素删除,返回是否成功
System.out.println("删除北京成功?" + remove); // 输出true
cities.clear(); // 清空集合
System.out.println("清空后元素个数:" + cities.size()); // 输出0
}
}
2. 遍历方式:两种常用场景(无索引!)
Set没有索引,不能用普通for循环,只能用“增强for循环”或“迭代器”,边遍历边删除优先用迭代器,避免并发修改异常。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetTraverseDemo {
public static void main(String[] args) {
Set<Integer> scores = new HashSet<>(Set.of(85, 92, 78, 92)); // 自动去重,实际为[85,92,78]
// 方式1:增强for循环(最简洁,仅遍历首选)
System.out.println("=== 增强for循环 ===");
for (Integer score : scores) {
System.out.println(score);
}
// 方式2:迭代器(边遍历边删除首选,安全无异常)
System.out.println("=== 迭代器遍历并删除 ===");
Iterator<Integer> it = scores.iterator();
while (it.hasNext()) { // 先判断是否有下一个元素
Integer score = it.next(); // 再获取元素(必须先判断)
if (score < 80) { // 删除小于80的分数
it.remove(); // 迭代器删除,避免ConcurrentModificationException
}
}
System.out.println("删除后集合:" + scores); // 输出[85, 92]
}
}
3. TreeSet专属:排序规则实现(面试高频)
TreeSet的核心是排序,必须明确排序规则,否则存自定义对象报错。下面用“学生成绩排序”举例,讲清自然排序和定制排序。
import java.util.Comparator;
import java.util.TreeSet;
// 学生实体类(实现Comparable接口,自然排序)
class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
// 自然排序:按成绩升序(从小到大)
@Override
public int compareTo(Student o) {
// 返回值规则:负数→当前对象小,0→相等(去重),正数→当前对象大
return this.score - o.score;
}
@Override
public String toString() {
return "姓名:" + name + ",成绩:" + score;
}
}
public class TreeSetSortDemo {
public static void main(String[] args) {
// 1. 自然排序(依赖Student的compareTo方法)
TreeSet<Student> studentSet1 = new TreeSet<>();
studentSet1.add(new Student("张三", 90));
studentSet1.add(new Student("李四", 85));
studentSet1.add(new Student("王五", 90)); // 成绩相同,被去重
System.out.println("=== 自然排序(成绩升序) ===");
for (Student s : studentSet1) {
System.out.println(s); // 输出[李四(85), 张三(90)]
}
// 2. 定制排序(用Comparator自定义规则,优先级高于自然排序)
TreeSet<Student> studentSet2 = new TreeSet<>(
new Comparator<Student>() {
// 定制排序:按成绩降序(从大到小)
@Override
public int compare(Student o1, Student o2) {
return o2.score - o1.score;
}
}
);
studentSet2.add(new Student("张三", 90));
studentSet2.add(new Student("李四", 85));
studentSet2.add(new Student("赵六", 95));
System.out.println("=== 定制排序(成绩降序) ===");
for (Student s : studentSet2) {
System.out.println(s); // 输出[赵六(95), 张三(90), 李四(85)]
}
}
}
四、进阶避坑:4个新手必踩的Set陷阱(面试高频)
Set的坑集中在“去重失效”和“排序报错”,这些都是面试官最爱问的考点,踩一次就记牢!
1. 陷阱1:HashSet存自定义对象,去重失效
新手用HashSet存自定义对象(比如Student、User)时,发现重复对象没被过滤——核心原因是没重写hashCode()和equals()方法,HashSet无法判断两个对象是否相等。
✅ 避坑方案:存自定义对象到HashSet,必须重写hashCode()和equals(),且逻辑一致(比如都按学号/ID判断):
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class User {
private String id; // 按ID判断是否重复
private String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
// 必须重写equals和hashCode,否则去重失效
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id); // 按ID判断相等
}
@Override
public int hashCode() {
return Objects.hash(id); // 按ID生成哈希值
}
@Override
public String toString() {
return "User{id='" + id + "', name='" + name + "'}";
}
}
public class HashSetDuplicateDemo {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
users.add(new User("U1001", "张三"));
users.add(new User("U1001", "张三三")); // ID重复,被去重
System.out.println(users); // 输出[User{id='U1001', name='张三'}]
}
}
2. 陷阱2:TreeSet存自定义对象,排序报错
用TreeSet存自定义对象时,如果对象没实现Comparable接口,也没传Comparator,会直接抛出ClassCastException(类型转换异常)。
✅ 避坑方案:二选一——要么让对象实现Comparable,要么创建TreeSet时传Comparator:
import java.util.TreeSet;
// 错误示例:User没实现Comparable,TreeSet无法排序
public class TreeSetErrorDemo {
public static void main(String[] args) {
TreeSet<User> users = new TreeSet<>();
users.add(new User("U1001", "张三")); // 直接报错:ClassCastException
}
}
// 正确示例:传Comparator自定义排序
public class TreeSetCorrectDemo {
public static void main(String[] args) {
TreeSet<User> users = new TreeSet<>((u1, u2) -> u1.getId().compareTo(u2.getId()));
users.add(new User("U1001", "张三"));
users.add(new User("U1002", "李四"));
System.out.println(users); // 正常输出
}
}
3. 陷阱3:混淆“无序”和“随机”
新手看到HashSet的遍历顺序和插入顺序不同,就以为是“随机顺序”——其实不然,HashSet的遍历顺序由元素的哈希值决定,只要元素不变、JDK版本不变,遍历顺序就固定,只是不保证和插入顺序一致。
✅ 避坑方案:需要“去重+保留插入顺序”,用LinkedHashSet(HashSet的子类),它用链表记录插入顺序:
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set1 = new LinkedHashSet<>();
set1.add("A");
set1.add("B");
set1.add("C");
System.out.println(set1); // 输出[A, B, C],保留插入顺序
Set<String> set2 = new HashSet<>();
set2.add("A");
set2.add("B");
set2.add("C");
System.out.println(set2); // 输出可能是[A, C, B],不保证插入顺序
}
}
4. 陷阱4:Set存null值的注意事项
HashSet和LinkedHashSet可以存一个null值(不可重复,只能存一个),但TreeSet不能存null值——因为TreeSet要对元素排序,null无法参与比较,会抛出NullPointerException:
import java.util.HashSet;
import java.util.TreeSet;
public class SetNullDemo {
public static void main(String[] args) {
// 正确:HashSet可以存一个null
HashSet<String> hashSet = new HashSet<>();
hashSet.add(null);
hashSet.add(null); // 重复,添加失败
System.out.println(hashSet); // 输出[null]
// 错误:TreeSet不能存null
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add(null); // 抛出NullPointerException
}
}
五、企业级实战:用户标签管理系统(Set版)
结合Set的去重和排序特点,实现用户标签管理系统:用HashSet存储用户标签(快速去重),用TreeSet排序展示标签,支持添加、删除、批量操作,完整代码可直接运行!
1. 实战需求
- 标签去重:添加标签自动过滤重复值,批量添加也能去重;
- 标签排序:展示标签时按字母顺序排序;
- 核心功能:新增用户、单个/批量添加标签、删除标签、查看标签。
2. 完整代码
import java.util.*;
// 1. 用户实体类(包含用户ID、姓名、标签集合)
class UserInfo {
private String userId;
private String userName;
// 用HashSet存储标签:快速去重
private Set<String> tags = new HashSet<>();
public UserInfo(String userId, String userName) {
this.userId = userId;
this.userName = userName;
}
// 添加单个标签
public boolean addTag(String tag) {
if (tag == null || tag.trim().isEmpty()) {
System.out.println("❌ 标签不能为空");
return false;
}
boolean isSuccess = tags.add(tag);
if (isSuccess) {
System.out.println("✅ 标签[" + tag + "]添加成功");
} else {
System.out.println("❌ 标签[" + tag + "]已存在,无需重复添加");
}
return isSuccess;
}
// 批量添加标签
public void addTags(Collection<String> newTags) {
System.out.println("\n=== 批量添加标签 ===");
for (String tag : newTags) {
addTag(tag);
}
}
// 删除标签
public boolean removeTag(String tag) {
boolean isSuccess = tags.remove(tag);
if (isSuccess) {
System.out.println("✅ 标签[" + tag + "]删除成功");
} else {
System.out.println("❌ 标签[" + tag + "]不存在");
}
return isSuccess;
}
// 获取排序后的标签(用TreeSet排序)
public Set<String> getSortedTags() {
return new TreeSet<>(tags); // TreeSet自动按字母升序排序
}
// 展示用户标签信息
public void showTags() {
System.out.println("\n=== " + userName + "(" + userId + ")的标签信息 ===");
if (tags.isEmpty()) {
System.out.println("暂无标签");
return;
}
// 展示排序后的标签
Set<String> sortedTags = getSortedTags();
System.out.print("已添加标签(按字母排序):");
for (String tag : sortedTags) {
System.out.print(tag + " ");
}
System.out.println("\n标签总数:" + tags.size());
}
// Getter(仅需要的字段)
public String getUserId() {
return userId;
}
}
// 2. 标签管理服务类(核心业务逻辑)
class TagManager {
// 存储所有用户信息:key=用户ID,value=用户对象
private Map<String, UserInfo> userMap = new HashMap<>();
// 新增用户
public void addUser(String userId, String userName) {
if (userMap.containsKey(userId)) {
System.out.println("❌ 用户ID[" + userId + "]已存在");
return;
}
userMap.put(userId, new UserInfo(userId, userName));
System.out.println("✅ 用户[" + userName + "]添加成功");
}
// 获取用户(不存在则返回null)
public UserInfo getUser(String userId) {
return userMap.get(userId);
}
}
// 3. 主程序(用户交互)
public class TagManagementSystem {
public static void main(String[] args) {
TagManager tagManager = new TagManager();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("\n===== 用户标签管理系统 =====");
System.out.println("1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签");
System.out.println("4. 给用户删除标签 5. 查看用户标签 6. 退出");
System.out.print("请输入操作序号:");
int choice = scanner.nextInt();
scanner.nextLine(); // 吸收换行符
switch (choice) {
case 1:
// 新增用户
System.out.print("请输入用户ID:");
String userId = scanner.nextLine();
System.out.print("请输入用户名:");
String userName = scanner.nextLine();
tagManager.addUser(userId, userName);
break;
case 2:
// 单个添加标签
System.out.print("请输入用户ID:");
String addUserId = scanner.nextLine();
UserInfo addUser = tagManager.getUser(addUserId);
if (addUser == null) {
System.out.println("❌ 未找到用户ID[" + addUserId + "]");
break;
}
System.out.print("请输入要添加的标签:");
String tag = scanner.nextLine();
addUser.addTag(tag);
break;
case 3:
// 批量添加标签
System.out.print("请输入用户ID:");
String batchUserId = scanner.nextLine();
UserInfo batchUser = tagManager.getUser(batchUserId);
if (batchUser == null) {
System.out.println("❌ 未找到用户ID[" + batchUserId + "]");
break;
}
System.out.print("请输入批量标签(用逗号分隔):");
String tagStr = scanner.nextLine();
List<String> tagList = Arrays.asList(tagStr.split(","));
batchUser.addTags(tagList);
break;
case 4:
// 删除标签
System.out.print("请输入用户ID:");
String delUserId = scanner.nextLine();
UserInfo delUser = tagManager.getUser(delUserId);
if (delUser == null) {
System.out.println("❌ 未找到用户ID[" + delUserId + "]");
break;
}
System.out.print("请输入要删除的标签:");
String delTag = scanner.nextLine();
delUser.removeTag(delTag);
break;
case 5:
// 查看标签
System.out.print("请输入用户ID:");
String showUserId = scanner.nextLine();
UserInfo showUser = tagManager.getUser(showUserId);
if (showUser == null) {
System.out.println("❌ 未找到用户ID[" + showUserId + "]");
break;
}
showUser.showTags();
break;
case 6:
System.out.println("退出系统");
scanner.close();
return;
default:
System.out.println("输入错误,请重新选择");
}
}
}
}
3. 运行效果
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:1
请输入用户ID:U001
请输入用户名:张三
✅ 用户[张三]添加成功
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:2
请输入用户ID:U001
请输入要添加的标签:Java
✅ 标签[Java]添加成功
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:3
请输入用户ID:U001
请输入批量标签(用逗号分隔):Java,Spring,MySQL,Java
=== 批量添加标签 ===
❌ 标签[Java]已存在,无需重复添加
✅ 标签[Spring]添加成功
✅ 标签[MySQL]添加成功
❌ 标签[Java]已存在,无需重复添加
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:5
请输入用户ID:U001
=== 张三(U001)的标签信息 ===
已添加标签(按字母排序):Java MySQL Spring
标签总数:3
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:4
请输入用户ID:U001
请输入要删除的标签:Spring
✅ 标签[Spring]删除成功
===== 用户标签管理系统 =====
1. 新增用户 2. 给用户添加标签 3. 给用户批量添加标签
4. 给用户删除标签 5. 查看用户标签 6. 退出
请输入操作序号:5
请输入用户ID:U001
=== 张三(U001)的标签信息 ===
已添加标签(按字母排序):Java MySQL
标签总数:2
六、核心总结(收藏这篇就够了)
- Set核心两特点:不可重复(去重)、无序(HashSet)/有序(TreeSet),无索引;
- 实现类选择:快速去重→HashSet,去重+排序→TreeSet,去重+保序→LinkedHashSet;
- HashSet去重关键:自定义对象必须重写
hashCode()和equals(),逻辑保持一致; - TreeSet排序关键:自定义对象要么实现
Comparable,要么传Comparator; - 避坑重点:TreeSet不能存null,LinkedHashSet兼顾去重和插入顺序,HashSet遍历顺序由哈希值决定;
- 面试考点:HashSet底层结构与去重原理、TreeSet排序方式、Set与List的核心区别。
福利时间!
这篇Set干货只是Java集合框架的冰山一角~ 更多Java核心知识点(集合源码解析、JVM调优、并发编程、Spring全家桶)、企业级实战项目、面试真题解析,我都整理在了公众号「咖啡Java研习室」里!
关注公众号回复【学习资料】,即可领取:
- 50页+Set底层原理思维导图(含HashSet去重逻辑、TreeSet排序机制);
- Set/HashSet/TreeSet面试高频题(2024最新版,含答案);
- 企业级去重场景解决方案文档。
👉 公众号:咖啡Java研习室(扫码关注,回复【学习资料】领取福利)
如果这篇文章对你有帮助,别忘了点赞+收藏+转发,让更多Java开发者少踩坑~ 评论区留言你遇到过的Set坑!