Skip to content

TreeSet 排序机制:Comparable vs Comparator

排序的本质:谁说了算

TreeSet 的排序规则由两个因素决定:

  1. 元素本身 —— 实现了 Comparable 接口(如 IntegerString
  2. 外部指定 —— 创建 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
常用 APIComparator.comparing(), reversed(), thenComparing()

一句话:Comparable 是元素的「天赋」,Comparator 是 TreeSet 的「选择」。


相关链接

基于 VitePress 构建