Day23 - 集合 - 2.Set集合

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 中存放自定义对象,要正确实现 hashCodeequals 方法(HashSet),或实现 Comparable(TreeSet)。

🏢 企业实战理解

  • 阿里巴巴:在用户唯一标识(如用户ID集合)处理时,利用 HashSet 快速去重,保证分布式消息系统中的幂等性。

  • 字节跳动:在推荐算法中,使用 TreeSet 对候选内容排序,比如按热度 + 发布时间多条件排序,TreeSet 高效处理动态排名。

  • Google:分布式缓存一致性时,用 HashSet 存储已完成的同步任务 ID,避免重复计算。

  • OpenAI:在 ChatGPT 服务层防止重复请求时,内部短时缓存会用 Set 做唯一请求标识。

❓ 面试题:
什么是 Set 集合?它有哪些典型的实现类?各自的特点是什么?

✅ 参考答案:

Set 是 Java 集合框架中的一种单列集合,它的核心特点是:不允许存储重复元素。Set 集合不提供索引,因此不能通过下标访问元素。

常见实现类:

  1. HashSet

    • 底层:基于 HashMap 实现。

    • 特点:元素无序、唯一;允许存放 null(最多一个)。

    • 时间复杂度:增删查 O(1)。

  2. LinkedHashSet

    • 底层:HashMap + 双向链表。

    • 特点:元素唯一且有插入顺序。

    • 应用场景:既要去重又要保证遍历顺序。

  3. TreeSet

    • 底层:红黑树(TreeMap 实现)。

    • 特点:元素唯一、自动排序(自然排序或比较器排序)。

    • 时间复杂度:增删查 O(logN)。

大厂加分点:

  • **HashSet 去重机制:**依赖元素的 hashCode + equals 方法;

  • **TreeSet 去重机制:**依赖 compareToComparator 比较规则,返回 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() 时通过 compareTocompare 方法决定位置,如果比较结果为 0,视为重复不添加。

🏢 企业实战理解

  • 腾讯云:安全风控模块中,使用 HashSet 存储已封禁 IP 列表,高效判断是否命中黑名单。

  • 美团:大促秒杀活动场景,用 Set 存储下单用户的唯一标识,防止刷单。

  • 亚马逊 AWS:S3 存储层中,用 HashSet 追踪数据块的唯一标识(block ID)去重,防止重复写入。

  • 字节跳动:在直播服务中使用 TreeSet 排序直播间列表(比如观众数、点赞数排序)。

 

2️⃣ TreeSet 排序机制题

❓ 面试题:
TreeSet 是如何保证元素有序的?TreeSet 和 HashSet 在去重时的区别是什么?

✅ 参考答案:

  • TreeSet 排序机制:
    TreeSet 内部基于红黑树(平衡二叉搜索树),插入元素时:
    1️⃣ 如果构造函数没有指定比较器,则要求元素实现 Comparable 接口,TreeSet 调用 compareTo() 方法确定顺序;
    2️⃣ 如果构造函数指定了比较器,则使用 Comparatorcompare() 方法进行排序。

  • TreeSet 去重机制:
    判断重复的依据是比较方法 compareTo()compare() 的返回值:

    • 返回 0:视为重复,不存储;

    • 非 0:视为不重复,存储。

  • HashSet 和 TreeSet 区别:

    对比点HashSetTreeSet
    排序不保证顺序元素自动排序
    去重机制hashCode + equalscompareTo 或 compare 返回值为 0 时去重
    性能O(1) 查找O(logN) 查找
    使用场景快速去重、无需顺序需要有序、按规则排序去重

场景题1:字节跳动——内容去重系统

题目描述:

字节跳动在搭建视频审核平台时,需要对每天上传的数百万条短视频内容进行标题去重。产品经理要求:

  • 相同标题不能重复入库;

  • 标题集合需支持频繁的查重验证;

  • 数据规模巨大但对插入顺序不关心。

你作为Java开发负责人,推荐了使用Set相关集合,请说明:

1️⃣ 为什么使用Set而不是ListMap来做去重?

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集合使用无参构造方法

    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序

  • 实现步骤

    1. 使用空参构造创建TreeSet集合

      • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的

    2. 自定义的Student类实现Comparable接口

      • 自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T o)方法

    3. 重写接口中的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 存储时需要进行比较(compareTocompare),而 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树(字典树)节省存储空间。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值