1. 概述

在编程中,我们经常需要对对象集合进行排序。如果希望根据多个字段对对象进行排序,排序逻辑可能会变得复杂。本教程将讨论几种不同的解决方案及其优缺点。

2. 示例:Person 类

首先定义一个包含两个字段(name 和 age)的 Person 类。在我们的示例中,我们将根据 name 然后是 age 对 Person 对象进行比较:

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

    // constructor
    // getters and setters
}

这里,我们在类上添加了 @Nonnull 注解以简化例子。但在生产代码中,可能需要处理可空字段的比较。

3. 使用 Comparator.compare()

Java 提供了 Comparator 接口,用于比较同类型对象的两个实例。我们可以实现其 compare(T o1, T o2) 方法,以自定义逻辑执行所需的比较。

3.1. 逐个检查不同字段

按顺序检查每个字段:

public class CheckFieldsOneByOne implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        int nameCompare = o1.getName().compareTo(o2.getName());
        if(nameCompare != 0) {
            return nameCompare;
        }
        return Integer.compare(o1.getAge(), o2.getAge());
    }
}

这里使用 String 类的 compareTo() 方法和 Integer 类的 compare() 方法依次比较 name 和 age 字段。

这种方法需要大量编码,并且在处理多个特殊情况时可能会变得复杂。当有更多的字段需要比较时,维护和扩展性较差。通常不建议在生产代码中使用这种方法。

3.2. 使用 Guava 的 ComparisonChain

首先,将 Google Guava 库的依赖项添加到 pom.xml 中:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

通过库中的 ComparisonChain 类简化逻辑:

public class ComparisonChainExample implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return ComparisonChain.start()
          .compare(o1.getName(), o2.getName())
          .compare(o1.getAge(), o2.getAge())
          .result();
    }
}

这里使用 compare(int left, int right)compare(Comparable<?> left, Comparable<?> right) 方法分别比较 name 和 age。

这种方法隐藏了比较细节,只暴露我们关心的内容——我们想要比较的字段以及它们的比较顺序。此外,由于库方法已经处理了 null 处理,所以无需额外逻辑,更易于维护和扩展。

3.3. 使用 Apache Commons 的 CompareToBuilder 进行排序

添加 Apache Commons 的依赖项到 pom.xml

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

同样,可以使用 Apache Commons 的 CompareToBuilder 减少样板代码:

public class CompareToBuilderExample implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return new CompareToBuilder()
          .append(o1.getName(), o2.getName())
          .append(o1.getAge(), o2.getAge())
          .build();
    }
}

这个方法与 Guava 的 ComparisonChain 类非常相似——它也隐藏了比较细节,易于维护和扩展

4. 使用 Comparator.comparing() 和 Lambda 表达式

从 Java 8 开始,Comparator 接口引入了一些静态方法,允许使用 Lambda 表达式创建 Comparator 实例。我们可以使用 comparing() 方法构建所需的 Comparator:

public static Comparator<Person> createPersonLambdaComparator() {
    return Comparator.comparing(Person::getName)
      .thenComparing(Person::getAge);
}

这种方法更加简洁易读,因为它直接使用了 Person 类的 getter。

同时,它保留了之前方法的可维护性和扩展性。此外,这里的 getter 是懒加载的,相比之前的即时计算,性能更好,更适合对延迟敏感且需要大量大数据比较的系统。

另外,这种方法仅使用核心 Java 类,不需要第三方库作为依赖。总的来说,这是最推荐的方法

5. 检查比较结果

让我们测试我们看到的四种比较器,并检查它们的行为。所有这些比较器都可以以相同的方式调用,应该产生相同的结果:

@Test
public void testComparePersonsFirstNameThenAge() {
    Person person1 = new Person("John", 21);
    Person person2 = new Person("Tom", 20);
    // Another person named John
    Person person3 = new Person("John", 22);

    List<Comparator<Person>> comparators =
      Arrays.asList(new CheckFieldsOneByOne(),
        new ComparisonChainExample(),
        new CompareToBuilderExample(),
        createPersonLambdaComparator());
    // All comparators should produce the same result
    for(Comparator<Person> comparator : comparators) {
        Assertions.assertIterableEquals(
          Arrays.asList(person1, person2, person3)
            .stream()
            .sorted(comparator)
            .collect(Collectors.toList()),
          Arrays.asList(person1, person3, person2));
    }
}

在这里,person1 的 name("John")与 person3 相同,但年龄更小(21 < 22),而 person3 的 name("John")在字典序上小于 person2 的 name("Tom")。因此,最终顺序是 person1、person3、person2。

此外,如果 Person 类的 class variable name 上没有 @Nonnull 注解,我们需要在除 Apache Commons 的 CompareToBuilder 以外的所有方法中添加额外的 null 处理逻辑(因为 Apache Commons 已内置了对 null 的处理)。

6. 总结

在这篇文章中,我们学习了在对对象集合进行多字段排序时的不同比较方法。

如往常一样,示例代码可在 GitHub 上找到。