Java 流 - 用空值排序

2,027 阅读3分钟

学习使用比较器nullsFirst()nullsLast()方法对Java列表或流进行排序可能包含空值,或者自定义对象可能有空字段值。

在比较过程中,如果不能处理空值,将在运行时引起*NullPointerException*。

1.简介

在本教程中,我们将学习以下方法的用法。

  • Comparator.nullsFirst()- 如果至少有一个值(被比较)是null ,则返回一个null友好的比较器。它认为null小于非null。如果比较的两边都是null ,则认为两边相等。
  • Comparator.nullsLast()- - 如果至少有一个值(被比较的)是null ,则返回一个null友好的比较器。它认为空值比非空值大。如果比较的两边都是null ,那么两者都被认为是相等的。

事实上,上述两个方法都返回一个NullComparator 的实例,该实例在比较两边的排序时增加了必要的空值检查。

这里,nullFirst 指的是null-first或null-last方法。而real 是指原始的比较器,两边都是非空值。

static final class NullComparator<T> 
	implements Comparator<T>, Serializable {
	...
	...
	public int compare(T a, T b) {
	    if (a == null) {
	        return (b == null) ? 0 : (nullFirst ? -1 : 1);
	    } else if (b == null) {
	        return nullFirst ? 1: -1;
	    } else {
	        return (real == null) ? 0 : real.compare(a, b);
	    }
	}
	...
	...
}

2.对自定义对象流的排序

让我们假设我们正在对一个Employee 对象的列表进行排序。我们想按出生日期对一个给定的雇员流进行排序。

public class Employee 
{
    private long id;
    private String name;
    private LocalDate dateOfBirth;
    private double salary;

    //constructors, setters and getters are hidden for brevity

    @Override
    public String toString() {
        return "Employee [id=" + id + ", name=" + name + ", dateOfBirth="
            + dateOfBirth + ", salary=" + salary + "]";
    }
}

2.1.列表中的几个自定义对象是空的

现在假设,有几个雇员对象是空的。在给定的例子中,我们在流中添加了两个null,以及3个雇员。

我们使用nullsFirst()方法进行排序,在排序后,空值将出现在开始的位置,然后雇员列表将按照出生日期进行排序

要按自然顺序排序,在对空值排序后,使用Comparator.nullsFirst( Comparator.naturalOrder() )

public class DateOfBirthComparator implements Comparator<Employee> {

    @Override
    public int compare(final Employee e1, final Employee e2) {
        return e1.getDateOfBirth().compareTo(e2.getDateOfBirth());
    }
}
List<Employee> sortedEmployeeList = getEmployeeListWithNullObjects()
    .stream()
    .sorted(Comparator.nullsFirst(new DateOfBirthComparator()))
    .collect(Collectors.toList());

System.out.println(sortedEmployeeList);

...
...

private static List<Employee> getEmployeeListWithNullObjects() {
    List<Employee> empList = new ArrayList<>();
    empList.add(new Employee(1, "A", LocalDate.of(1992, 1, 1), 30000d));
    empList.add(null);
    empList.add(new Employee(3, "C", LocalDate.of(1992, 9, 1), 50000d));
    empList.add(null);
    empList.add(new Employee(5, "E", LocalDate.of(1992, 8, 1), 60000d));
    return empList;
}

程序输出。

[null, 
null, 
Employee [id=1, name=A, dateOfBirth=1992-01-01, salary=30000.0], 
Employee [id=5, name=E, dateOfBirth=1992-08-01, salary=60000.0], 
Employee [id=3, name=C, dateOfBirth=1992-09-01, salary=50000.0]]

2.2.自定义对象的字段值是空的

在某些情况下,我们可能有一个对象流,其中的对象是非空的,但它们的字段值可能是空的。例如,在雇员流中,所有雇员对象可能都是非空的,但少数雇员可能没有出生日期信息。

在这种情况下,我们有两个选择来对雇员实例进行排序。

2.2.1.在自定义比较器中实现额外的空值检查

为了避免NullPointerException,我们可以在自定义比较器类中自己编写空检查。

public class DateOfBirhComparator implements Comparator<Employee> {

    @Override
    public int compare(final Employee e1, final Employee e2) {
        if (e1.getDateOfBirth() == null && e2.getDateOfBirth() == null) {
            return 0;
        } else if(e1.getDateOfBirth() == null) {
            return -1;
        } else if(e2.getDateOfBirth() == null) {
            return 1;
        } else {
            return e1.getDateOfBirth().compareTo(e2.getDateOfBirth());
        }
    }
}

然后我们使用这个比较器来进行排序。注意输出结果,前两条记录的出生日期字段为空。

sortedEmployeeList = getEmployeeListWithNullDates().stream()
    .sorted(new DateOfBirhComparator())
    .collect(Collectors.toList());

private static List<Employee> getEmployeeListWithNullDates() {
    List<Employee> empList = new ArrayList<>();
    empList.add(new Employee(1, "A", LocalDate.of(1991, 1, 1), 30000d));
    empList.add(new Employee(2, "B", null, 20000d));
    empList.add(new Employee(3, "C", LocalDate.of(1992, 8, 1), 50000d));
    empList.add(new Employee(4, "D", LocalDate.of(2001, 3, 11), 50000d));
    empList.add(new Employee(5, "E", null, 60000d));
    return empList;
}
[
Employee [id=2, name=B, dateOfBirth=null, salary=20000.0], 
Employee [id=5, name=E, dateOfBirth=null, salary=60000.0], 
Employee [id=1, name=A, dateOfBirth=1991-01-01, salary=30000.0], 
Employee [id=3, name=C, dateOfBirth=1992-08-01, salary=50000.0], 
Employee [id=4, name=D, dateOfBirth=2001-03-11, salary=50000.0]
]

请注意,如果我们想同时处理Employee对象为空或dateOfBirth字段为空的情况,我们可以使用 DateOfBirhComparatorNullComparator的组合。

sortedEmployeeList = getEmployeeListWithNullDates().stream()
    .sorted(Comparator.nullsFirst(new DateOfBirhComparator()))
    .collect(Collectors.toList());

2.2.2.使用比较器.比较()

我们可以用Lambda语法以如下方式提供内联的自定义比较器。

sortedEmployeeList = getEmployeeListWithNullDates().stream()
    .sorted(Comparator.comparing(Employee::getDateOfBirth,
        	Comparator.nullsFirst(Comparator.naturalOrder())))
    .collect(Collectors.toList());

上述代码也将提供与自定义比较器相同的功能。

3.用空号按自然顺序进行排序

对于内置的Java类型,如基元封装类字符串,我们可以使用它们的自然排序来实现排序目的。我们唯一需要注意的是,任何null 值都不应该以NullPointerException来破坏代码。

3.1.将空值排在最后

使用Comparator.nullsLast(Comparator.naturalOrder()) ,将非空值排序在开始,空值排序在最后。

List<String> names = Arrays.asList("C", null, "B", "D", null, "A", "E");

List<String> sortedList = names.stream()
    .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
    .collect(Collectors.toList());

System.out.println(sortedList);  //[A, B, C, D, E, null, null]

3.2.在开始中对空值进行排序

使用Comparator.nullsFirst(Comparator.naturalOrder()) ,对begin中的空值和last中的非空值进行排序。

List<String> names = Arrays.asList("C", null, "B", "D", null, "A", "E");

List<String> sortedList = names.stream()
    .sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
    .collect(Collectors.toList());

System.out.println(sortedList);    //[null, null, A, B, C, D, E]

4.结语

在这个Java流教程中,我们学习了如何对字符串流和自定义对象流进行排序。该排序逻辑将能够处理流中可能有空值或流中的对象可能有空字段的情况。

我们学习了NullComparator 如何提供内部空值检查,我们也可以使用自定义的比较器类来添加。

学习愉快!!

下载源代码