杰瑞科技汇

Java Comparator排序如何实现自定义规则?

为什么需要 Comparator

我们来看一个简单的例子,理解为什么需要它。

Java Comparator排序如何实现自定义规则?-图1
(图片来源网络,侵删)

假设我们有一个 Student 类,我们想根据学生的年龄进行排序。

class Student {
    private String name;
    private int age;
    public Student(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 "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}

如果我们想对 Student 对象列表进行排序,直接调用 Collections.sort()List.sort() 会怎么样?

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 22));
        students.add(new Student("Bob", 20));
        students.add(new Student("Charlie", 25));
        // 这行代码会编译失败!
        // Collections.sort(students); 
    }
}

编译失败! 错误信息大概是: java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable

这是因为 Collections.sort() 方法需要一个前提:列表中的元素必须实现了 Comparable 接口。Comparable 接口定义了对象之间的“自然排序”(natural ordering)规则。

Java Comparator排序如何实现自定义规则?-图2
(图片来源网络,侵删)

Comparable vs Comparator 的核心区别:

特性 Comparable Comparator
定义位置 被排序的类内部实现。 类外部定义一个独立的比较器。
目的 定义类的“自然排序”规则。String 类按字典序排序。 定义一个临时的、自定义的排序规则。
方法 int compareTo(T o) int compare(T o1, T o2)
灵活性 一个类只能有一个 compareTo 方法,灵活性差。 可以创建任意多个不同的 Comparator,非常灵活。
使用场景 当对象有“天然”的、公认的排序方式时(如数字大小、日期先后)。 当需要多种排序方式,或者无法/不想修改类源码时。

如何使用 Comparator

Comparator 是一个函数式接口(从 Java 8 开始),位于 java.util 包中,它提供了一种非常灵活的方式来定义排序规则。

使用匿名内部类(传统方式)

这是在 Java 8 之前最常见的方式。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 22));
        students.add(new Student("Bob", 20));
        students.add(new Student("Charlie", 25));
        // 1. 按年龄升序排序
        Comparator<Student> ageComparator = new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // s1 的年龄小于 s2 的年龄,返回负数(s1 排在 s2 前面)
                // s1 的年龄大于 s2 的年龄,返回正数(s1 排在 s2 后面)
                // 如果相等,返回 0
                return Integer.compare(s1.getAge(), s2.getAge());
                // 也可以直接写: return s1.getAge() - s2.getAge();
            }
        };
        Collections.sort(students, ageComparator);
        System.out.println("按年龄升序排序:");
        students.forEach(System.out::println);
        // 2. 按年龄降序排序
        Comparator<Student> ageDescComparator = new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // 只需颠倒比较顺序即可
                return Integer.compare(s2.getAge(), s1.getAge());
                // 或者: return s2.getAge() - s1.getAge();
            }
        };
        Collections.sort(students, ageDescComparator);
        System.out.println("\n按年龄降序排序:");
        students.forEach(System.out::println);
    }
}

使用 Lambda 表达式(Java 8+ 推荐)

由于 Comparator 是一个函数式接口,我们可以用更简洁的 Lambda 表达式来替代匿名内部类。

Java Comparator排序如何实现自定义规则?-图3
(图片来源网络,侵删)

compare(T o1, T o2) 方法可以看作是一个接受两个参数、返回一个整数的函数。

import java.util.ArrayList;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 22));
        students.add(new Student("Bob", 20));
        students.add(new Student("Charlie", 25));
        // 1. 按年龄升序排序
        // (s1, s2) -> s1.getAge() - s2.getAge() 是一个 Lambda 表达式
        students.sort((s1, s2) -> s1.getAge() - s2.getAge());
        // 或者更安全地使用 Integer.compare
        // students.sort((s1, s2) -> Integer.compare(s1.getAge(), s2.getAge()));
        System.out.println("按年龄升序排序 (Lambda):");
        students.forEach(System.out::println);
        // 2. 按年龄降序排序
        students.sort((s1, s2) -> s2.getAge() - s1.getAge());
        System.out.println("\n按年龄降序排序 (Lambda):");
        students.forEach(System.out::println);
    }
}

注意: 从 Java 8 开始,List 接口本身提供了 sort(Comparator<? super E> c) 方法,所以可以直接调用 list.sort(),而不需要再通过 Collections.sort()


Comparator 的链式调用(thenComparing

在实际应用中,我们经常需要先按一个主条件排序,如果主条件相同,再按次条件排序。Comparator 提供了 thenComparing 方法来实现这种链式调用。

示例:先按年龄升序,如果年龄相同,再按姓名字典序排序。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 22));
        students.add(new Student("Bob", 20));
        students.add(new Student("Charlie", 25));
        students.add(new Student("David", 20)); // 和 Bob 年龄相同
        // 链式调用
        // 先按年龄升序
        Comparator<Student> comparator = Comparator.comparingInt(Student::getAge)
                // 如果年龄相同,则按姓名的字典序升序
                .thenComparing(Student::getName);
        students.sort(comparator);
        System.out.println("先按年龄升序,年龄相同则按姓名升序:");
        students.forEach(System.out::println);
    }
}

输出结果:

先按年龄升序,年龄相同则按姓名升序:
Student{name='Bob', age=20}
Student{name='David', age=20}
Student{name='Alice', age=22}
Student{name='Charlie', age=25}

Student::getAge 是方法引用,是 Lambda 表达式的一种简写形式,Student::getName 也是同理。


Comparator 的常用静态方法(Java 8+)

Comparator 接口本身提供了许多非常有用的静态工厂方法,可以极大地简化代码。

静态方法 功能 示例
comparing( Function<T, U> keyExtractor) 根据提取的 key 进行自然排序(升序)。 Comparator.comparing(Student::getAge)
comparingInt( ToIntFunction<T> keyExtractor) 专门用于 int 类型的 key,避免装箱/拆箱,效率更高。 Comparator.comparingInt(Student::getAge)
comparingLong( ToLongFunction<T> keyExtractor) 专门用于 long 类型的 key Comparator.comparingLong(Student::getAge)
comparingDouble( ToDoubleFunction<T> keyExtractor) 专门用于 double 类型的 key Comparator.comparingDouble(Student::getAge)
naturalOrder() 对实现了 Comparable 的对象进行自然排序。 Comparator.naturalOrder()
reverseOrder() 对实现了 Comparable 的对象进行反向排序。 Comparator.reverseOrder()
nullsFirst(Comparator<? super T> comparator) null 值视为“最小”值,排在最前面。 Comparator.nullsFirst(...)
nullsLast(Comparator<? super T> comparator) null 值视为“最大”值,排在最后面。 Comparator.nullsLast(...)

示例:处理 null

假设我们的学生列表中可能包含 null 元素。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Alice", 22));
        students.add(null);
        students.add(new Student("Bob", 20));
        students.add(null);
        // 直接排序会抛出 NullPointerException
        // students.sort(Comparator.comparingInt(Student::getAge));
        // 使用 nullsFirst 将 null 值排在最前面
        Comparator<Student> nullFirstComparator = Comparator.nullsFirst(
                Comparator.comparingInt(Student::getAge)
        );
        students.sort(nullFirstComparator);
        System.out.println("使用 nullsFirst 排序:");
        students.forEach(System.out::println);
    }
}

输出结果:

使用 nullsFirst 排序:
null
null
Student{name='Bob', age=20}
Student{name='Alice', age=22}

总结与最佳实践

  1. 选择 Comparable 还是 Comparator

    • 如果这个类的“自然排序”是固定且公认的(如 String, Date, Integer),让它实现 Comparable 接口。
    • 如果排序规则多种多样、临时需要,或者你无法修改被排序类的源码,请使用 Comparator
  2. 现代 Java 的首选:

    • 方法引用 (Student::getAge) 比完整的 Lambda 表达式 (s -> s.getAge()) 更简洁、更具可读性。
    • 静态工厂方法 (Comparator.comparingInt()) 是创建 Comparator 实例最推荐、最安全、最高效的方式。
    • 链式调用 (thenComparing) 让处理多级排序变得非常优雅。
  3. 代码示例(最佳实践版):

import java.util.*;
import java.util.stream.Collectors;
public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("Alice", 22),
                new Student("Bob", 20),
                new Student("Charlie", 25),
                new Student("David", 20),
                null
        );
        // 1. 按年龄升序(推荐方式)
        List<Student> sortedByAgeAsc = students.stream()
                .sorted(Comparator.comparingInt(Student::getAge))
                .collect(Collectors.toList());
        System.out.println("按年龄升序:");
        sortedByAgeAsc.forEach(System.out::println);
        // 2. 按年龄降序(推荐方式)
        List<Student> sortedByAgeDesc = students.stream()
                .sorted(Comparator.comparingInt(Student::getAge).reversed())
                .collect(Collectors.toList());
        System.out.println("\n按年龄降序:");
        sortedByAgeDesc.forEach(System.out::println);
        // 3. 先按年龄升序,再按姓名降序(链式调用)
        List<Student> sortedByAgeAndName = students.stream()
                .sorted(Comparator.comparingInt(Student::getAge)
                        .thenComparing(Student::getName, Comparator.reverseOrder()))
                .collect(Collectors.toList());
        System.out.println("\n先按年龄升序,再按姓名降序:");
        sortedByAgeAndName.forEach(System.out::println);
        // 4. 处理 null 值
        List<Student> sortedWithNulls = students.stream()
                .sorted(Comparator.nullsFirst(
                        Comparator.comparingInt(Student::getAge)
                ))
                .collect(Collectors.toList());
        System.out.println("\n处理 null 值:");
        sortedWithNulls.forEach(System.out::println);
    }
}

掌握 Comparator 是成为 Java 高级程序员的必经之路,它在数据处理、算法实现和日常开发中都扮演着至关重要的角色。

分享:
扫描分享到社交APP
上一篇
下一篇