按结束时间升序、结束时间相同时再按开始时间排序,本来只是两个排序键。遇到 null 后添加 nullsLast,却又报方法引用错误,问题可能不在静态方法,而在比较器的类型。

两次错误对应两个问题

最初直接按字段排序:

Comparator.comparing(TaskDto::getEndTime)
    .thenComparing(TaskDto::getStartTime);

时间字段有 null 时,默认自然顺序比较可能出现空指针异常。随后若照搬字符串示例:

Comparator.comparing(
    TaskDto::getEndTime,
    Comparator.nullsLast(String::compareTo)
);

而 getEndTime 实际返回 Date,比较器就无法匹配。方法引用相关编译诊断不等于必须把 getter 改成 static;应先核对提取键的返回类型。

使用 Date 比较器的完整示例

import java.util.*;

public class DateSortDemo {
    static final class TaskDto {
        private final String id;
        private final Date endTime;
        private final Date startTime;

        TaskDto(String id, Date endTime, Date startTime) {
            this.id = id;
            this.endTime = endTime;
            this.startTime = startTime;
        }

        Date getEndTime() { return endTime; }
        Date getStartTime() { return startTime; }
    }

    public static void main(String[] args) {
        List<TaskDto> tasks = new ArrayList<>(Arrays.asList(
            new TaskDto("A", null, new Date(1)),
            new TaskDto("B", new Date(2), null),
            new TaskDto("C", null, null)
        ));

        Comparator<TaskDto> comparator = Comparator
            .comparing(TaskDto::getEndTime,
                Comparator.nullsLast(Date::compareTo))
            .thenComparing(TaskDto::getStartTime,
                Comparator.nullsLast(Date::compareTo));

        tasks.sort(comparator);
        tasks.forEach(task -> System.out.println(task.id));
        // 按上述规则,顺序为 B、A、C
    }
}

这里的 Date 数值只用于构造排序样本,不表达业务时区。Stream 的 sorted 也能接收同一个 comparator;List.sort 会修改原列表,Stream.sorted 则形成排序后的流。

nullsLast 只包装它接收到的值

传给 comparing 的第二个参数比较的是提取出的字段,不是 TaskDto 本身。如果列表允许出现整个对象为 null,要再在外层使用 Comparator.nullsLast(comparator),或者在输入边界拒绝空对象。Java 8 Comparator

thenComparing 只在前一个比较结果相等时生效,不是“只要结束时间为空就随意用开始时间替代结束时间”。若两个结束时间都为 null,它们在第一关键字上相等,再比较开始时间。

空值排在后面是业务规则

结束时间为空可以表示尚未完成、未知或缺失,各自的排序要求可能不同。还应考虑所有键都相等时的展示顺序,必要时再加稳定的业务标识作为第三关键字,而不是为消除异常就默认采用某个业务含义。