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 上找到。