TreeSet 排序机制:Comparable vs Comparator
排序的本质:谁说了算
TreeSet 的排序规则由两个因素决定:
- 元素本身 —— 实现了
Comparable接口(如Integer、String) - 外部指定 —— 创建 TreeSet 时传入
Comparator
两者只能选一个。Comparator 优先级更高——如果创建 TreeSet 时传了比较器,元素的 Comparable 实现就被忽略了。
方式一:自然排序(Comparable)
元素自身定义排序规则,TreeSet 直接拿来用:
java
// Integer 实现了 Comparable
TreeSet<Integer> set1 = new TreeSet<>();
set1.add(3); set1.add(1); set1.add(2);
System.out.println(set1); // [1, 2, 3]
// String 实现了 Comparable(按字典序)
TreeSet<String> set2 = new TreeSet<>();
set2.add("cherry"); set2.add("apple"); set2.add("banana");
System.out.println(set2); // [apple, banana, cherry]如何让自定义对象支持自然排序
java
public class Person implements Comparable<Person> {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
// 定义排序规则:先按年龄升序,年龄相同按姓名升序
@Override
public int compareTo(Person other) {
if (this.age != other.age) {
return this.age - other.age; // 年龄小的排前面
}
return this.name.compareTo(other.name); // 姓名升序
}
}
// 使用
TreeSet<Person> people = new TreeSet<>();
people.add(new Person("张三", 25));
people.add(new Person("李四", 22));
people.add(new Person("王五", 25));
System.out.println(people);
// [Person{name=李四, age=22}, Person{name=张三, age=25}, Person{name=王五, age=25}]Comparable 的陷阱:修改后排序乱掉
java
TreeSet<Person> set = new TreeSet<>();
set.add(new Person("张三", 25));
set.add(new Person("李四", 22));
Person zhang = new Person("张三", 25);
zhang.age = 18; // ❌ 修改了 age,TreeSet 内部排序就乱了!
// 因为 TreeSet 内部按 age 排序,修改后位置不对了
set.contains(zhang); // 可能 false(树结构被破坏)
set.remove(zhang); // 可能删不掉永远不要修改已在 TreeSet 中的元素的排序字段。
方式二:自定义排序(Comparator)
不想修改类本身?创建 TreeSet 时传入比较器:
java
// 按年龄升序
TreeSet<Person> byAge = new TreeSet<>(
Comparator.comparingInt(p -> p.age)
);
byAge.add(new Person("张三", 25));
byAge.add(new Person("李四", 22));
System.out.println(byAge); // [李四(22), 张三(25)]
// 按姓名长度降序
TreeSet<String> byLenDesc = new TreeSet<>(
Comparator.comparingInt(String::length).reversed()
);
byLenDesc.add("hi"); byLenDesc.add("banana"); byLenDesc.add("apple");
System.out.println(byLenDesc); // [banana, apple, hi]Comparator 的强大组合
java
// 复杂排序:先按类型分组,再按价格升序
TreeSet<Product> catalog = new TreeSet<>(
Comparator.comparing(Product::getCategory)
.thenComparing(Product::getPrice)
);Comparator 覆盖 Comparable
java
public class User implements Comparable<User> {
String name;
int score;
@Override
public int compareTo(User other) {
// 自然排序:按 score 升序
return this.score - other.score;
}
}
// ❌ 但如果你想按 name 排序:
TreeSet<User> byName = new TreeSet<>(
Comparator.comparing(u -> u.name) // ← Comparator 优先
);
byName.add(new User("Bob", 80));
byName.add(new User("Alice", 90));
System.out.println(byName); // Bob, Alice(按 name)
// 注意:User 的 compareTo 方法完全被忽略了降序:最简单的方式
java
// 方式 1:Collections.reverseOrder()
TreeSet<Integer> desc = new TreeSet<>(Collections.reverseOrder());
desc.addAll(Arrays.asList(1, 3, 2, 5, 4));
System.out.println(desc); // [5, 4, 3, 2, 1]
// 方式 2:reversed()(JDK 11+)
TreeSet<Integer> desc2 = new TreeSet<>(Comparator.reverseOrder());
// 方式 3:自定义 Comparator
TreeSet<Integer> desc3 = new TreeSet<>((a, b) -> b - a);reversed() 的原理
java
// reversed() 返回一个反转的比较器
Comparator<Integer> reversed = Comparator.naturalOrder().reversed();
// 等价于
Comparator<Integer> reversed2 = (a, b) -> b.compareTo(a);null 的两种态度
TreeSet 默认不允许 null
java
TreeSet<String> set = new TreeSet<>();
set.add(null); // NullPointerException因为 TreeSet 需要比较元素来确定顺序。null 无法比较。
如果非要允许 null
java
// 使用接受 null 的 Comparator
TreeSet<String> nullable = new TreeSet<>(
Comparator.nullsLast(Comparator.naturalOrder())
);
nullable.add("apple");
nullable.add(null); // ✅ null 排在最后
nullable.add("banana");
System.out.println(nullable); // [apple, banana, null]| 比较器 | null 处理 |
|---|---|
| 自然排序 | ❌ 抛 NPE |
nullsFirst() | ✅ null 排最前 |
nullsLast() | ✅ null 排最后 |
实际应用场景
场景 1:排行榜
java
TreeSet<Player> leaderboard = new TreeSet<>(
Comparator.comparingInt(Player::getScore).reversed()
);
leaderboard.add(new Player("张三", 1500));
leaderboard.add(new Player("李四", 2000));
leaderboard.add(new Player("王五", 1800));
System.out.println("冠军: " + leaderboard.first()); // 李四(2000)场景 2:日程管理
java
TreeSet<LocalDateTime> schedule = new TreeSet<>();
schedule.add(LocalDateTime.of(2024, 3, 15, 9, 0));
schedule.add(LocalDateTime.of(2024, 3, 15, 14, 0));
schedule.add(LocalDateTime.of(2024, 3, 15, 9, 30));
// 第一个日程就是今天的第一个
System.out.println("下一个日程: " + schedule.higher(LocalDateTime.now()));总结
| 排序方式 | 设置位置 | 适用场景 |
|---|---|---|
| 自然排序 | 元素类实现 Comparable | 无法修改类源码时 |
| 自定义排序 | 创建时传 Comparator | 多种排序需求 |
| 降序排列 | reverseOrder() 或 reversed() | 需要逆序 |
| 要点 | 说明 |
|---|---|
| 优先级 | Comparator > Comparable |
| 修改风险 | 不要修改 TreeSet 中元素的排序字段 |
| null 处理 | 自然排序不支持;可用 nullsFirst/Last |
| 常用 API | Comparator.comparing(), reversed(), thenComparing() |
一句话:Comparable 是元素的「天赋」,Comparator 是 TreeSet 的「选择」。
