1. 概述
Java 8 为 Comparator 接口带来了多项增强,包括一系列非常实用的静态方法,在为集合定义排序规则时特别有用。
Comparator 接口也能很好地结合 Java 8 的 lambda 表达式。关于 lambda 和 Comparator 的详细解释可以参考这里,而 Comparator 和排序的应用场景可以参考这里。
在本教程中,我们将深入探讨 Java 8 为 Comparator 接口引入的几个核心方法。
2. 准备工作
2.1 示例 Bean 类
为便于后续演示,我们先创建一个 Employee 类,用它的字段进行比较和排序:
public class Employee {
String name;
int age;
double salary;
long mobile;
// 构造方法、getter 和 setter
}
2.2 测试数据
我们还需要一个员工数组,用于存储各种测试场景的排序结果:
employees = new Employee[] { ... };
employees 数组的初始顺序为:
[Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
整个教程中,我们将使用不同方法对这个 Employee 数组进行排序。
为验证测试结果,我们准备了几组预排序数组,用于与不同场景下的排序结果(即 employees 数组)进行对比:
@Before
public void initData() {
sortedEmployeesByName = new Employee[] {...};
sortedEmployeesByNameDesc = new Employee[] {...};
sortedEmployeesByAge = new Employee[] {...};
// ...
}
3. 使用 Comparator.comparing
本节将介绍 Comparator.comparing 静态方法的几种变体。
3.1 基础键选择器变体
Comparator.comparing 静态方法接收一个排序键 Function,并返回一个针对包含该键类型的 Comparator:
static <T,U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T,? extends U> keyExtractor)
实际使用时,我们以 Employee 的 name 字段作为排序键,将其方法引用作为 Function 参数传入。返回的 Comparator 即可用于排序:
@Test
public void whenComparing_thenSortedByName() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByName));
}
排序后,employees 数组按 name 字段升序排列:
[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
3.2 带自定义 Comparator 的键选择器变体
另一种变体允许通过提供自定义 Comparator 来覆盖排序键的自然顺序:
static <T,U> Comparator<T> comparing(
Function<? super T,? extends U> keyExtractor,
Comparator<? super U> keyComparator)
我们修改前面的测试:通过传入一个按名称降序排序的 Comparator 作为第二个参数,覆盖 name 字段的自然排序:
@Test
public void whenComparingWithComparator_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(
Employee::getName, (s1, s2) -> {
return s2.compareTo(s1);
});
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}
结果按 name 字段降序排列:
[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]
3.3 使用 Comparator.reversed
在现有 Comparator 上调用 reversed() 方法,会返回一个新 Comparator,其排序顺序与原 Comparator 相反。
我们使用按 name 排序的 Comparator,通过 reversed() 反转排序顺序,实现按 name 降序排序:
@Test
public void whenReversed_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparatorReversed
= employeeNameComparator.reversed();
Arrays.sort(employees, employeeNameComparatorReversed);
assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}
结果同样按 name 降序排列:
[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]
3.4 使用 Comparator.comparingInt
还有一个专门处理 int 类型键的方法 comparingInt。我们用它按 age 字段排序:
@Test
public void whenComparingInt_thenSortedByAge() {
Comparator<Employee> employeeAgeComparator
= Comparator.comparingInt(Employee::getAge);
Arrays.sort(employees, employeeAgeComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByAge));
}
排序后,employees 数组按 age 升序排列:
[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
3.5 使用 Comparator.comparingLong
类似地,使用 comparingLong 处理 long 类型键。我们按 mobile 字段排序:
@Test
public void whenComparingLong_thenSortedByMobile() {
Comparator<Employee> employeeMobileComparator
= Comparator.comparingLong(Employee::getMobile);
Arrays.sort(employees, employeeMobileComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByMobile));
}
排序后,数组按 mobile 升序排列:
[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001)]
3.6 使用 Comparator.comparingDouble
同样,使用 comparingDouble 处理 double 类型键。我们按 salary 字段排序:
@Test
public void whenComparingDouble_thenSortedBySalary() {
Comparator<Employee> employeeSalaryComparator
= Comparator.comparingDouble(Employee::getSalary);
Arrays.sort(employees, employeeSalaryComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesBySalary));
}
排序后,数组按 salary 升序排列:
[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
4. 在 Comparator 中使用自然顺序
自然顺序由 Comparable 接口的实现定义。关于 Comparator 和 Comparable 的区别可参考这篇文章。
我们让 Employee 类实现 Comparable 接口,以便测试 Comparator 的 naturalOrder 和 reverseOrder 方法:
public class Employee implements Comparable<Employee>{
// ...
@Override
public int compareTo(Employee argEmployee) {
return name.compareTo(argEmployee.getName());
}
}
4.1 使用自然顺序
naturalOrder 方法返回符合类型签名的自然顺序 Comparator:
static <T extends Comparable<? super T>> Comparator<T> naturalOrder()
基于 name 字段比较的逻辑,我们用 naturalOrder 获取按自然顺序排序的 Comparator:
@Test
public void whenNaturalOrder_thenSortedByName() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> naturalOrder();
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByName));
}
排序后,数组按 name 自然顺序排列:
[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
4.2 使用逆自然顺序
类似地,使用 reverseOrder 方法生成与 naturalOrder 顺序相反的 Comparator:
@Test
public void whenReverseOrder_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> reverseOrder();
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}
排序后,数组按 name 逆序排列:
[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]
5. 在 Comparator 中处理 null 值
本节介绍 nullsFirst 和 nullsLast 方法,它们在排序时处理 null 值,将 null 值放在序列开头或末尾。
5.1 null 值优先处理
我们在 employees 数组中随机插入 null 值:
[Employee(name=John, age=25, salary=3000.0, mobile=9922001),
null,
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
null,
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
nullsFirst 方法返回的 Comparator 会将所有 null 值放在排序序列的开头:
@Test
public void whenNullsFirst_thenSortedByNameWithNullsFirst() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparator_nullFirst
= Comparator.nullsFirst(employeeNameComparator);
Arrays.sort(employeesArrayWithNulls,
employeeNameComparator_nullFirst);
assertTrue(Arrays.equals(
employeesArrayWithNulls,
sortedEmployeesArray_WithNullsFirst));
}
排序后,null 值排在最前:
[null,
null,
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
5.2 null 值最后处理
nullsLast 方法返回的 Comparator 会将所有 null 值放在排序序列的末尾:
@Test
public void whenNullsLast_thenSortedByNameWithNullsLast() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparator_nullLast
= Comparator.nullsLast(employeeNameComparator);
Arrays.sort(employeesArrayWithNulls, employeeNameComparator_nullLast);
assertTrue(Arrays.equals(
employeesArrayWithNulls, sortedEmployeesArray_WithNullsLast));
}
排序后,null 值排在最后:
[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
null,
null]
6. 使用 Comparator.thenComparing
thenComparing 方法允许我们通过指定多个排序键实现字典式排序。
我们使用另一个 Employee 数组:
someMoreEmployees = new Employee[] { ... };
数组初始顺序为:
[Employee(name=Jake, age=25, salary=3000.0, mobile=9922001),
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Ace, age=22, salary=3000.0, mobile=6423001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
我们先按 age 排序,age 相同再按 name 排序:
@Test
public void whenThenComparing_thenSortedByAgeName(){
Comparator<Employee> employee_Age_Name_Comparator
= Comparator.comparing(Employee::getAge)
.thenComparing(Employee::getName);
Arrays.sort(someMoreEmployees, employee_Age_Name_Comparator);
assertTrue(Arrays.equals(someMoreEmployees, sortedEmployeesByAgeName));
}
排序结果:先按 age 升序,age 相同则按 name 升序:
[Employee(name=Ace, age=22, salary=3000.0, mobile=6423001),
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Jake, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
现在使用 thenComparingInt 变体,调整排序顺序为:先按 name,name 相同再按 age:
@Test
public void whenThenComparing_thenSortedByNameAge() {
Comparator<Employee> employee_Name_Age_Comparator
= Comparator.comparing(Employee::getName)
.thenComparingInt(Employee::getAge);
Arrays.sort(someMoreEmployees, employee_Name_Age_Comparator);
assertTrue(Arrays.equals(someMoreEmployees,
sortedEmployeesByNameAge));
}
排序结果:先按 name 升序,name 相同则按 age 升序:
[Employee(name=Ace, age=22, salary=3000.0, mobile=6423001),
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Jake, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]
类似地,thenComparingLong 和 thenComparingDouble 分别用于处理 long 和 double 类型的排序键。
7. 总结
本文详细介绍了 Java 8 为 Comparator 接口引入的几项核心功能。
与往常一样,完整源代码可在 GitHub 上获取。