Java Set集合终极指南:去重+排序底层原理+实战避坑,面试开发双通关!

24 阅读16分钟

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个关键点:

  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. 完整代码

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

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

  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坑!