CodeGym /课程 /JAVA 25 SELF /Comparator 接口:创建与使用

Comparator 接口:创建与使用

JAVA 25 SELF
第 29 级 , 课程 3
可用

1. 引言

在实际开发中,很少只需要一种比较对象的方式。设想你有一个用户列表:有时你想按名字排序,有时按年龄排序,有时又按姓氏长度排序。再比如,你手上的类并非你自己编写,因而无法在其中添加 compareTo。针对这些场景,Java 提供了 Comparator 接口。

何时 Comparable 不够用

  • 类不可修改(例如它来自第三方库)。
  • 需要多种排序方式(按不同字段)。
  • 希望将比较逻辑与类本身解耦(例如在程序的不同部分采用不同排序)。

类比
如果说 Comparable 是对象内置的“自然顺序”,那么 Comparator 就是一个外部“裁判”,能够按任意标准评价你的对象:今天按名字,明天按年龄,后天按名字长度。

2. Comparator 接口:语法与契约

接口声明

public interface Comparator<T> {
    int compare(T o1, T o2);
}

方法 compare 应返回:

  • 负数:如果第一个对象“小于”第二个对象。
  • 0:如果二者相等。
  • 正数:如果第一个对象“大于”第二个对象。

契约与 Comparable 相同,只是现在比较的是两个对象,而不是通过 compareTo 比较“当前对象”和“另一个对象”。

示例:按姓氏排序的比较器

假设有一个 Person 类:

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // 构造函数与 getter
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
}

创建一个按姓氏排序的比较器:

import java.util.Comparator;

public class LastNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return a.getLastName().compareTo(b.getLastName());
    }
}

注意:StringcompareTo 方法按字典序比较字符串。

3. 使用 Comparator:集合排序

使用比较器进行排序

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Anna", "Kostetskaya", 25));
        people.add(new Person("Boris", "Novak", 20));
        people.add(new Person("Viktoriya", "Bell", 22));

        // 按姓氏排序
        Collections.sort(people, new LastNameComparator());

        for (Person p : people) {
            System.out.println(p.getLastName() + " " + p.getFirstName());
        }
    }
}

结果:

Novak Boris
Kostetskaya Anna
Bell Viktoriya

使用比较器按年龄排序

即使类已经按姓名实现了 Comparable,也可以再创建一个按年龄的比较器:

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return Integer.compare(a.getAge(), b.getAge());
    }
}

用法同上:

Collections.sort(people, new AgeComparator());

结果:

Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)

示例:“临时”选择比较器

Collections.sort(people, new LastNameComparator()); // 按姓氏
Collections.sort(people, new AgeComparator());      // 按年龄

4. 匿名类与 Lambda 表达式

可以“临时”创建比较器,而无需声明单独的类。

匿名类

Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        return a.getFirstName().compareTo(b.getFirstName());
    }
});

Lambda 表达式

Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));

或者使用列表方法 List.sort 更简洁:

people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
  • 匿名类是旧方法,冗长。
  • Lambda 更现代且简洁。

5. 示例:按不同标准排序

按姓氏长度排序

Comparator<Person> byLastNameLength = (a, b) ->
        Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);

先按年龄,再按名字(多级)

Comparator<Person> byAgeThenName = (a, b) -> {
    int cmp = Integer.compare(a.getAge(), b.getAge());
    if (cmp != 0) return cmp;
    return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);

在搜索中使用比较器(示例)

比较器不仅用于排序,还可用于在已排序集合中搜索:

// people 必须按年龄排序!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
    System.out.println("找到年龄为 22 的人:" + people.get(idx));
}

6. 最佳实践与使用 Comparator 的要点

不要违反契约

  • 如果 compare(a, b) 返回 0,那么 compare(b, a) 也必须返回 0
  • 如果 compare(a, b) > 0,则 compare(b, a) 必须 < 0
  • 考虑可能出现的 null 值(见下文)。

别忘了 equals 和 hashCode

虽然比较器可以“自定义”比较对象,但对于 TreeSet 等结构或在 TreeMap 中查找键时,比较器的比较逻辑最好与 equals 保持一致。否则可能出现意外结果:两个不同对象按比较器相等,但按 equals 不相等。

考虑 null 的排序

如果字段可能为 null,请使用“现成”的辅助方法:

Comparator<Person> byLastNameNullSafe = Comparator.comparing(
    Person::getLastName,
    Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);

7. 实用细节

表格:Comparable 与 Comparator 对比

Comparable Comparator
在哪里实现? 在类本身 在独立类/Lambda 中
方法
int compareTo(T o)
int compare(T o1, T o2)
方案数量 只有一个“自然” 不限,按需定义
用法
Collections.sort(list)
Collections.sort(list, comp)
可用于第三方类?

示例:降序排序

可以手动反转顺序:

Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);

或使用 reversed()

Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());

8. 使用 Comparator 的常见错误

错误 1:违反比较契约。 如果你忽略了 compare(a, b)compare(b, a) 的结果必须符号相反,或者返回随意的值(例如直接做差 a.getAge() - b.getAge(),这可能溢出),结果将不可预测。使用 Integer.compare 而不是相减——更安全。

错误 2:忽略 null 值。 如果用于比较的字段可能为 null,务必处理这种情况(例如通过 Comparator.nullsFirst/Comparator.nullsLast),否则很容易在意想不到的时候得到 NullPointerException

错误 3:不稳定的排序标准。 如果比较器对同一对象对返回不同的值(例如使用随机数或高度可变的字段),排序行为可能会变得混乱。

错误 4:与 equals 不一致。 如果 compare(a, b) == 0,但 a.equals(b)false,那么诸如 TreeSetTreeMap 之类的集合可能不会按你的预期工作。最好让比较器的相等与 equals 的相等相一致。

错误 5:对第三方类不提供比较器就排序。 如果你尝试对没有实现 Comparable 的“第三方”类进行排序、且又未传入 Comparator,将得到编译错误。请显式传入比较器。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION