TreeSet 属于 Set 接口实现类,自带去重 + 自动排序;和 HashSet、LinkedHashSet 去重机制完全不一样,不依赖 hashCode () 和 equals (),依靠比较规则判定重复。 两种排序方案:自然排序(Comparable)、比较器排序(Comparator)
一、TreeSet 基础特点
- 底层:红黑树(平衡二叉搜索树)
- 两大核心特性:✅ 元素不重复(去重)、✅ 元素自动排序
- 无索引,不能用普通 for 循环遍历;遍历方式同样是迭代器、增强 for、forEach (Lambda)
- 不能存入 null,会抛出空指针异常(和 HashSet、LinkedHashSet 区分)
- 线程不安全
- 排序规则二选一:
- 自然排序:元素类实现
Comparable接口(默认方式) - 比较器排序:构造 TreeSet 时传入
Comparator比较器(优先级高于自然排序)
- 自然排序:元素类实现
课堂重点区分:
- HashSet:无序,哈希表,靠 hashCode+equals 去重
- LinkedHashSet:存取有序,哈希表 + 双向链表,靠 hashCode+equals 去重
- TreeSet:大小排序,红黑树,靠比较方法返回值去重
判定重复的规则(必考)
调用比较方法,返回 0 → 判定为重复元素,不存入集合
- 返回正数:当前元素大,放到右子树
- 返回负数:当前元素小,放到左子树
- 返回 0:相等,去重舍弃
二、方式 1:自然排序 Comparable(默认)
使用无参构造
new TreeSet<>(),要求存入的实体类实现Comparable<E>接口,重写compareTo()方法。 JDK 自带类:Integer、String 已经实现 Comparable,可以直接放进 TreeSet; 自定义类(Student)必须手动实现 Comparable,否则直接抛 ClassCastException 类型转换异常
示例:Student 实体类,按照年龄升序排序,年龄相同按 id 升序
import java.util.Objects; // 实现Comparable接口,指定比较类型 class Student implements Comparable<Student>{ private Integer id; private String name; private int age; public Student() {} public Student(Integer id, String name, int age) { this.id = id; this.name = name; this.age = age; } // get、set public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } 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{id=" + id + ", name='" + name + "', age=" + age + "}"; } // 核心:重写compareTo,定义排序规则 @Override public int compareTo(Student o) { // 规则:优先按年龄升序 int res = this.age - o.age; // 如果年龄相等,再比较id,id升序 res = res == 0 ? this.id - o.id : res; return res; // 【降序写法】 return o.age - this.age; } }测试代码:
TreeSet<Student> treeSet = new TreeSet<>(); treeSet.add(new Student(1001,"张三",18)); treeSet.add(new Student(1003,"李四",16)); treeSet.add(new Student(1002,"王五",18)); // Lambda遍历 treeSet.forEach(System.out::println);输出顺序:李四 (16) → 张三 (18) → 王五 (18)
课堂要点:如果只比较 age,age 相同返回 0,就会判定为同一个学生,直接去重;所以多字段比较是常用写法。
三、方式 2:比较器排序 Comparator(优先级更高)
使用带参构造
new TreeSet<>(Comparator比较器)优势:不用修改实体类代码,灵活切换排序规则;同一个实体类可以写多种排序逻辑 支持:匿名内部类、Lambda 表达式(课堂重点,Lambda 简化写法)
// 1.匿名内部类写法 TreeSet<Student> tree1 = new TreeSet<>(new Comparator<Student>() { @Override public int compare(Student s1, Student s2) { // 按年龄降序,年龄相同按名字排序 int res = s2.getAge() - s1.getAge(); res = res == 0 ? s1.getName().compareTo(s2.getName()) : res; return res; } }); // 2.Lambda简写(课堂推荐简洁写法) TreeSet<Student> tree2 = new TreeSet<>((s1,s2)->{ int res = s2.getAge() - s1.getAge(); res = res == 0 ? s1.getName().compareTo(s2.getName()) : res; return res; });优先级:Comparator 比较器排序 > Comparable 自然排序,两个都写的时候,以 Comparator 为准。
四、三种 Set 实现类完整对比表
| 集合 | 底层结构 | 顺序特点 | 去重依据 | 能否存 null | 效率 |
|---|---|---|---|---|---|
| HashSet | 哈希表 (数组 + 链表 + 红黑树) | 无序 | hashCode()+equals() | ✅ 最多 1 个 | 读写最快 |
| LinkedHashSet | 哈希表 + 双向链表 | 存取有序(添加顺序) | hashCode()+equals() | ✅ 最多 1 个 | 中等,维护链表 |
| TreeSet | 红黑树 | 元素大小排序 | compareTo /compare 返回值 | ❌ 不能存 null | 较慢,需要排序 |
五、TreeSet 高频易错点
- 存入自定义对象,没有指定排序规则 → 抛出
ClassCastException - 判定重复不靠 equals,比较方法返回 0 就直接去重,这点和 HashSet 完全不同,非常容易混淆
- 不能存入 null,会空指针;HashSet 可以存一个 null
- 排序不是 “存取顺序”,是数值、属性的大小顺序,不要和 LinkedHashSet 的有序搞混
- 比较器 Comparator 支持 Lambda,因为是函数式接口,符合之前 Lambda 的知识点
- 红黑树是平衡二叉树,保证查询、增删时间复杂度 O (logn),不会退化成链表
六、选择使用场景
- 只需要去重、不在乎顺序:优先 HashSet
- 需要去重,同时保留添加顺序:LinkedHashSet
- 需要去重,并且自动按属性大小排序:TreeSet
七、速记口诀
TreeSet 红黑树,排序去重两不误; Comparable 自然排,实体类里重写 compareTo; Comparator 比较器,构造传入更灵活; 返回零值就去重,和 Hash 规则不一样; 空值不能往里放,优先比较器来做主。