Java 8 Comparator指南
1. 概述
Java 8 对Comparator接口进行了几项增强,包括一些静态函数,这些函数在为集合排序时非常有用。 Comparator接口还可以有效地利用 Java 8 lambda。关于 lambda 和Comparator的详细解释可以在这里 找到,关于Comparator和排序应用的编年史可以在这里 找到。 在本教程中,我们将探讨为Java 8中的Comparator接口引入的几个函数。
2. 入门
2.1. 示例 Bean 类
对于本教程中的示例,让我们创建一个Employee bean 并使用它的字段进行比较和排序:
public class Employee {
String name;
int age;
double salary;
long mobile;
// constructors, getters & setters
}
2.2. 我们的测试数据
我们还将创建一个员工数组,用于在整个教程的各种测试用例中存储我们类型的结果:
employees = new Employee[] { ... };
Employee元素的初始排序将是:
[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静态函数接受排序键函数并返回包含排序键的类型的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数组值按名称排序:
[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作为Comparator.comparing的第二个参数来覆盖按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上调用时,实例方法Comparator.reversed返回一个新的Comparator,它反转原始的排序顺序。 我们将使用Comparator按Name对员工进行排序并将其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
还有一个函数Comparator.comparingInt,它与Comparator.comparing做同样的事情,但它只需要int选择器。让我们用一个按Age排序Employee的例子来试试:
@Test
public void whenComparingInt_thenSortedByAge() {
Comparator<Employee> employeeAgeComparator
= Comparator.comparingInt(Employee::getAge);
Arrays.sort(employees, employeeAgeComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByAge));
}
排序后,employees数组值的顺序如下:
[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
与我们对int键所做的类似,让我们看一个使用Comparator.comparingLong的示例,通过Mobile字段对Employee数组进行排序来考虑long类型的排序键:
@Test
public void whenComparingLong_thenSortedByMobile() {
Comparator<Employee> employeeMobileComparator
= Comparator.comparingLong(Employee::getMobile);
Arrays.sort(employees, employeeMobileComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByMobile));
}
排序后,employees数组值具有以下顺序,以mobile为 key:
[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
同样,就像我们对int和long键所做的那样,让我们看一个使用Comparator.comparingDouble的示例,通过按Salary字段对Employee数组进行排序来考虑double类型的排序键:
@Test
public void whenComparingDouble_thenSortedBySalary() {
Comparator<Employee> employeeSalaryComparator
= Comparator.comparingDouble(Employee::getSalary);
Arrays.sort(employees, employeeSalaryComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesBySalary));
}
排序后,employees数组值按以下顺序排列,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字段比较员工的逻辑,让我们使用此函数获取一个Comparator,该比较器按自然顺序对Employee数组进行排序:
@Test
public void whenNaturalOrder_thenSortedByName() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> naturalOrder();
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByName));
}
排序后,employees数组值的顺序如下:
[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. 使用反向自然顺序
与我们使用naturalOrder的方式类似,我们将使用reverseOrder方法生成一个Comparator ,该方法将产生与naturalOrder示例中的Employee相反的员工排序:
@Test
public void whenReverseOrder_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> reverseOrder();
Arrays.sort(employees, employeeNameComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}
排序后,employees数组值的顺序如下:
[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. 在比较器中考虑空值
在本节中,我们将介绍nullsFirst和nullsLast函数,它们在排序中考虑null值,并将null值保留在排序序列的开头或结尾。
5.1. 首先考虑空值
让我们在Employee数组中随机插入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));
}
排序后,employees数组值的顺序如下:
[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));
}
排序后,employees数组值的顺序如下:
[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,后跟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)]
现在我们可以使用 thenComparing 的另一个版本,thenComparingInt方法是将字典顺序更改为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));
}
排序后,employees数组值的顺序如下:
[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排序键。