👉 公众号:咖啡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个关键点:

  1. 哈希值定位:元素存入时,先调用hashCode()方法得到哈希值,再通过哈希值计算数组存储位置(下标);

  2. 哈希冲突解决:若两个元素哈希值对应下标相同(哈希冲突),用链表串起来;链表长度超过8时,自动转红黑树(提升查询效率);

  3. 去重判断逻辑:先通过哈希值找存储位置,再用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实现类选择口诀

  1. 仅去重,不关心顺序 → HashSet(效率优先);

  2. 去重+排序 → TreeSet(功能优先);

  3. 去重+保留插入顺序 → 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. 完整代码

👉 公众号:咖啡Java研习室(回复【学习资料】领取福利)

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

六、核心总结(收藏这篇就够了)

  1. Set核心两特点:不可重复(去重)、无序(HashSet)/有序(TreeSet),无索引;

  2. 实现类选择:快速去重→HashSet,去重+排序→TreeSet,去重+保序→LinkedHashSet;

  3. HashSet去重关键:自定义对象必须重写hashCode()equals(),逻辑保持一致;

  4. TreeSet排序关键:自定义对象要么实现Comparable,要么传Comparator

  5. 避坑重点:TreeSet不能存null,LinkedHashSet兼顾去重和插入顺序,HashSet遍历顺序由哈希值决定;

  6. 面试考点:HashSet底层结构与去重原理、TreeSet排序方式、Set与List的核心区别。

福利时间!

这篇Set干货只是Java集合框架的冰山一角~ 更多Java核心知识点(集合源码解析、JVM调优、并发编程、Spring全家桶)、企业级实战项目、面试真题解析,我都整理在了公众号「咖啡Java研习室」里!

关注公众号回复【学习资料】,即可领取:

  • 50页+Set底层原理思维导图(含HashSet去重逻辑、TreeSet排序机制);

  • Set/HashSet/TreeSet面试高频题(2024最新版,含答案);

  • 企业级去重场景解决方案文档。

👉 公众号:咖啡Java研习室(扫码关注,回复【学习资料】领取福利)

如果这篇文章对你有帮助,别忘了点赞+收藏+转发,让更多Java开发者少踩坑~ 评论区留言你遇到过的Set坑!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐