Java 8 Comparator.comparing() 指南
1.概述
Java 8对Comparator接口引入了一些增强功能,包括一些静态函数,这些函数在为集合制定排序顺序时具有很大的作用。
Comparator 接口也可以有效地利用Java 8的lambdas。关于lambdas和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[] { ... };
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[] {...};
// ...
}
一如既往,请随时参考我们的GitHub链接,以获取完整的代码。
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数组的值是按名字顺序排列的。
[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字段排序的自然顺序,作为Comparator.comparing的第二个参数,将名字按降序排序。
@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));
}
我们可以看到,结果是按姓名降序排序的。
[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对雇员进行排序,并reverse,使雇员按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选择器。让我们用一个例子来试试,我们用employees按age排序:
@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来考虑一个long类型的排序键,通过对employees数组按mobile字段进行排序。
@Test
public void whenComparingLong_thenSortedByMobile() {
Comparator<Employee> employeeMobileComparator
= Comparator.comparingLong(Employee::getMobile);
Arrays.sort(employees, employeeMobileComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesByMobile));
}
在排序之后,employees array的值有以下的顺序,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
同样,正如我们对int和long键所做的那样,让我们看一个使用Comparator.comparingDouble的例子,来考虑一个double类型的排序键,通过对employees数组按salary字段排序:
@Test
public void whenComparingDouble_thenSortedBySalary() {
Comparator<Employee> employeeSalaryComparator
= Comparator.comparingDouble(Employee::getSalary);
Arrays.sort(employees, employeeSalaryComparator);
assertTrue(Arrays.equals(employees, sortedEmployeesBySalary));
}
在排序之后,employees array的值有以下的顺序,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,该函数将employees数组按自然顺序排序:
@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例子中的employees相比,它将产生一个反向的排序:
@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.首先考虑空值
让我们在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,该函数将所有的nulls保持在排序序列的开头:
@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,该函数将所有的nulls保留在排序序列的最后:
@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的排序键。
7.结语
本文是关于Java 8中引入的Comparator接口的几项功能的指南。
像往常一样,源代码可以在Github上找到。