Java 8 Comparator.comparing()指南

2023/06/09

1. 概述

Java 8对Comparator接口进行了一些增强,包括一些静态方法,这些方法在为集合排序时非常有用。

Comparator接口还可以有效地利用Java 8 lambda。lambdas和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 测试数据

我们还将创建一个Employee数组,用于在整个教程的各种测试用例中存储我们类型的结果:

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数组)进行比较。

让我们声明其中的一些数组:

@BeforeEach
void initData() {
    sortedEmployeesByName = new Employee[] {...};
    sortedEmployeesByNameDesc = new Employee[] {...};
    sortedEmployeesByAge = new Employee[] {...};
    
    // ...
}

3. 使用Comparator.comparing

在本节中,我们介绍Comparator.comparing静态方法的重载。

3.1 key选择器变体

Comparator.comparing静态方法接收排序key的Function作为参数,并返回包含排序key的类型的Comparator:

static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)

要看到它的作用,我们将使用Employee中的name字段作为排序key,并将其方法引用作为Function类型的参数传递。从中返回的Comparator用于排序:

@Test
void whenComparing_thenSortedByName() {
	Comparator<Employee> employeeNameComparator = Comparator.comparing(Employee::getName);
    
	Arrays.sort(employees, employeeNameComparator);
    
	assertArrayEquals(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 key选择器和比较器变体

另一种方法可以通过提供一个为排序key创建自定义排序机制的Comparator来覆盖key的默认自然排序:

static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)

因此,让我们修改上面的测试。我们将通过提供一个用于按降序对名称进行排序的Comparator作为Comparator.comparing的第二个参数来覆盖按名称字段排序的自然顺序:

@Test
void whenComparingWithComparator_thenSortedByNameDesc() {
	Comparator<Employee> employeeNameComparator = Comparator.comparing(
        Employee::getName, (s1, s2) -> {
        	return s2.compareTo(s1);
        });
    
	Arrays.sort(employees, employeeNameComparator);
    
	assertArrayEquals(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按姓名对员工进行排序并将其反转,以便员工按姓名的降序排序:

@Test
void whenReversed_thenSortedByNameDesc() {
	Comparator<Employee> employeeNameComparator = Comparator.comparing(Employee::getName);
	Comparator<Employee> employeeNameComparatorReversed = employeeNameComparator.reversed();
    
	Arrays.sort(employees, employeeNameComparatorReversed);
    
	assertArrayEquals(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.4 使用Comparator.comparingInt

还有一个方法Comparator.comparingInt,它与Comparator.comparing做同样的事情,但它只接收int选择器。我们通过一个按年龄对员工进行排序的例子演示:

@Test
void whenComparingInt_thenSortedByAge() {
	Comparator<Employee> employeeAgeComparator = Comparator.comparingInt(Employee::getAge);
    
	Arrays.sort(employees, employeeAgeComparator);
    
	assertArrayEquals(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字段对员工数组进行排序来演示long类型的排序key:

@Test
void whenComparingLong_thenSortedByMobile() {
	Comparator<Employee> employeeMobileComparator = Comparator.comparingLong(Employee::getMobile);
    
	Arrays.sort(employees, employeeMobileComparator);
    
	assertArrayEquals(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字段对员工数组进行排序来演示double类型的排序key:

@Test
void whenComparingDouble_thenSortedBySalary() {
	Comparator<Employee> employeeSalaryComparator = Comparator.comparingDouble(Employee::getSalary);
    
	Arrays.sort(employees, employeeSalaryComparator);
    
	assertArrayEquals(employees, sortedEmployeesBySalary);
}

排序后,employees数组的顺序如下,以salary为排序key:

[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. 在比较器中考虑自然顺序

我们可以通过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()

鉴于上述基于姓名字段比较员工的逻辑,让我们使用此方法获取一个比较器,该比较器按自然顺序对员工数组进行排序:

@Test
void whenNaturalOrder_thenSortedByName() {
	Comparator<Employee> employeeNameComparator = Comparator.naturalOrder();
    
	Arrays.sort(employees, employeeNameComparator);
    
	assertArrayEquals(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示例中的相反的员工排序:

@Test
void whenReverseOrder_thenSortedByNameDesc() {
	Comparator<Employee> employeeNameComparator = Comparator.reverseOrder();
    
	Arrays.sort(employees, employeeNameComparator);
    
	assertArrayEquals(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方法,它们在排序中会考虑空值,并将空值保留在排序序列的开头或结尾。

5.1 首先考虑空值

让我们在员工数组中随机插入空值:

[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,该比较器将所有空值保留在排序序列的开头:

@Test
void whenNullsFirst_thenSortedByNameWithNullsFirst() {
	Comparator<Employee> employeeNameComparator = Comparator.comparing(Employee::getName);
    
	Comparator<Employee> employeeNameComparator_nullFirst = Comparator.nullsFirst(employeeNameComparator);
    
	Arrays.sort(employeesArrayWithNulls, employeeNameComparator_nullFirst);

	assertArrayEquals(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 最后考虑空值

nullsLast方法返回一个比较器,它将所有空值保留在排序序列的末尾:

@Test
void whenNullsLast_thenSortedByNameWithNullsLast() {
	Comparator<Employee> employeeNameComparator = Comparator.comparing(Employee::getName);
    
	Comparator<Employee> employeeNameComparator_nullLast = Comparator.nullsLast(employeeNameComparator);
    
	Arrays.sort(employeesArrayWithNulls, employeeNameComparator_nullLast);

	assertArrayEquals(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方法允许我们通过按特定顺序提供多个排序key来设置值的字典顺序。

让我们看一下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)]

然后我们编写一个比较器序列,首先按年龄排序,然后按名称排序,并查看该数组的顺序:

@Test
void whenThenComparing_thenSortedByAgeName() {
	Comparator<Employee> employee_Age_Name_Comparator = Comparator.comparing(Employee::getAge).thenComparing(Employee::getName);
    
	Arrays.sort(someMoreEmployees, employee_Age_Name_Comparator);
    
	assertArrayEquals(someMoreEmployees, sortedEmployeesByAgeName);
}

在这里,排序将按年龄完成,对于具有相同年龄的值,排序将按名称完成。我们可以在排序后得到的数组中看到这一点:

[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
void whenThenComparing_thenSortedByNameAge() {
	Comparator<Employee> employee_Name_Age_Comparator = Comparator.comparing(Employee::getName).thenComparingInt(Employee::getAge);
    
	Arrays.sort(someMoreEmployees, employee_Name_Age_Comparator);

	assertArrayEquals(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排序key。

7. 总结

本文是Java 8中为Comparator接口引入的几个功能的指南。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章