Contents

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,它反转原始的排序顺序。 我们将使用ComparatorName对员工进行排序并将其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

同样,就像我们对intlong键所做的那样,让我们看一个使用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接口的naturalOrderreverseOrder函数:

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. 在比较器中考虑空值

在本节中,我们将介绍nullsFirstnullsLast函数,它们在排序中考虑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)]

类似地,函数thenComparingLongthenComparingDouble分别用于使用longdouble排序键。