Skip to content

比较与排序

前置知识

在阅读本章之前,你需要了解:Java 基础语法、类与对象、集合框架(如 List、ArrayList)、以及基本的循环和条件语句。

为什么需要比较与排序?

在日常开发中,我们常常需要把一堆东西“排个序”:比如按照年龄给用户排序,或者按照工资给员工排序。你有没有碰到过这种情况:列表里的元素明明是对象,没法直接用 <> 来比较,却又必须“说服”程序帮你排个序?

这时候,Java 提供了两把“钥匙”——ComparableComparator。它们就像两种不同的对话方式,帮你告诉 Java 该如何比较两个对象的大小。了解它们,不光能让你写出更灵活的代码,还能避免那些看似简单却让人头疼的排序陷阱。我们一步步来探索,让比较和排序变得不再难懂。


Comparable 和 Comparator:两种“约定俗成”的比较方式

Comparable — 让对象自己会比较

简单定义:
Comparable 接口定义了对象之间的自然排序规则。一个类实现了 Comparable,它的实例就“自带了比较能力”,可以用来排序。

为什么需要它?
当你觉得一种“默认”的排序方法足够用(比如按年龄或按名字字母序),让对象自己实现比较是最直接的。

基础用法:
实现 Comparable<T> 接口,并重写 compareTo(T o) 方法。


代码示例 1 — 用 Comparable 实现简单排序

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 实现 compareTo 方法,按照年龄升序排序
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        Collections.sort(people); // 直接用 Collections.sort 排序,内部调用 compareTo

        for (Person p : people) {
            System.out.println(p);
        }
    }
}

这段代码做了什么?

  1. Person 类实现了 Comparable<Person>,告诉 Java “我自己会比大小”。
  2. compareTo 方法简洁地使用了 Integer.compare 来比较年龄。
  3. main 方法中,用 Collections.sort 直接对 Person 列表排序,排序顺序就是 compareTo 里定义的。
  4. 打印结果会显示按年龄从小到大的顺序:Bob(25), Alice(30), Charlie(35)。

Comparator — 给排序提供另一种声音

简单定义:
Comparator 是一个独立的比较器,定义了两元素之间的比较规则。你可以创建多个 Comparator,分别为同一个类提供不同的排序依据。

为什么需要它?
当你有不止一种排序需求,或者不想修改原始类代码时,Comparator 显得尤为灵活。比如你想按名字排序,而不是年龄。


代码示例 2 — 使用 Comparator 实现多种排序方式

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));
        
        // 按名字字母顺序排序 Comparator
        Comparator<Person> nameComparator = new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getName().compareTo(p2.getName());
            }
        };

        Collections.sort(people, nameComparator);

        System.out.println("按名字排序:");
        for (Person p : people) {
            System.out.println(p);
        }
    }
}

这段代码做了什么?

  1. Person 类未实现 Comparable,保持它自然的“无序”。
  2. 创建了一个按名字排序的 Comparator,实现了 compare 方法。
  3. 使用带有 Comparator 参数的 Collections.sort,实现按名字排序。
  4. 输出结果是名字的字母顺序:Alice(30), Bob(25), Charlie(35)。

自定义排序中的一些“高级技巧”和“稳定性”问题

当我们自定义排序规则时,常会遇到“排序稳定性”的概念。简而言之,稳定排序的意思是:如果两个元素在排序标准下被判断为相等,它们在排序后的相对顺序保持不变。

为什么这个很重要呢?想象一下,有一个用户列表先按注册时间排序(先后顺序),后来又按年龄排序。如果排序算法是稳定的,那么年龄相同的用户之间仍保持先注册的先排前面,这种行为在实际中非常实用。


代码示例 3 — 结合 Comparator 实现多级排序 (先按年龄升序,年龄相同时按名字字母序)

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Charlie", 30));
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("David", 25));

        // 多级比较器:先按年龄,年龄相同再按名字比较
        Comparator<Person> ageThenNameComparator = Comparator
            .comparingInt(Person::getAge)
            .thenComparing(Person::getName);

        Collections.sort(people, ageThenNameComparator);

        System.out.println("多级排序(年龄升序,名字字母序):");
        for (Person p : people) {
            System.out.println(p);
        }
    }
}

这段代码做了什么?

  1. 使用 Java 8 的方法引用和链式 Comparator API 组合比较。
  2. 首先按年龄升序排序,如果两个年龄相同,则再按名字字母顺序排序。
  3. 保持了排序的稳定性和清晰度。
  4. 输出结果会是:Bob(25), David(25), Alice(30), Charlie(30)。

对比总结:Comparable VS Comparator

特性ComparableComparator
定义方式在类内部定义 compareTo 方法在类外定义单独的比较器类或匿名类
用途对象的“自然排序”不同排序规则的灵活实现
修改对象代码需要修改对象类,适合默认排序的场合不需要修改对象类,适合多种排序需求
Java8 支持仅实现接口通过 lambda 和方法引用支持更简洁写法
应用场景单一排序行为(如数字、字符串等)多重排序,复杂定制排序

在实战中,若只有一种排序需求,建议实现 Comparable。若排序规则多样且不适合修改原类,使用 Comparator 灵活组合。


💡 实战建议

  • 优先实现 Comparable,给对象设定合理的默认排序,便于使用标准的集合排序方法。
  • Comparator 实现灵活多变的排序方案,特别是当排序标准众多,或不能修改第三方类时。
  • 注意排序稳定性,尤其在多级排序中,确保按重要性顺序设计比较链。
  • 利用 Java 8 的 Comparator 静态方法和链式调用,写出简洁、易读的排序代码。

⚠️ 常见陷阱

  • compareTocompare 返回值意义弄反。
    负数表示左边元素小于右边元素,0 表示相等,正数表示左边大于右边。弄反会导致排序异常,有时甚至不会报错。

  • Comparator实现不一致导致遇到错误。
    比如,传入的比较器不符合equals的一般约定,导致 TreeSetTreeMap 行为异常。

  • 忘了实现 Comparable 导致调用 Collections.sort 出错。
    如果类没有实现 Comparable,而你调用了不带比较器的排序方法,会抛出 ClassCastException

  • 没考虑排序的稳定性需求。
    在某些场景要保证稳定性时,选错排序算法或写错比较规则会破坏数据顺序。


延伸思考

  • 假如你有一个复杂的对象,有多个字段需要排序,你会设计多少个 Comparator
  • 编写比较逻辑时,你如何保证遵守约定(自反性、传递性、一致性)避免排序异常?
  • Java 的新特性如 Stream.sorted(),它内部用的排序是稳定的吗?这对业务逻辑有什么影响?

小结

  • Comparable 是让对象自己会“比较”的接口,适合默认排序。
  • Comparator 让你可以灵活写出多个比较规则,按需调用。
  • 理解排序稳定性,避免在多级排序时产生意外结果。
  • 现代 Java 版本提供了更简洁优雅的比较器写法,建议掌握。
  • 避免常见陷阱,写出的代码既健壮又易维护。

希望这次的讲解能帮你理清 Java 中比较和排序的关键路径。既有理论的“引擎”,也有实战的“调校”,让我们带着这些工具,轻松应对各种排序挑战吧!