Comparator:如何使用nullsFirst和stream api?

Comparator: How to use nullsFirst and stream api?

我正在努力编写一个 Comparator 来处理空字段。

我有这样的pojo:

import static java.util.Comparator.comparingInt;

import java.math.BigDecimal;
import java.util.Comparator;
import lombok.Builder;
import lombok.Data;
import org.jetbrains.annotations.NotNull;

@Data
@Builder
public class Pojo implements Comparable<Pojo> {
  private Integer id;
  private BigDecimal cost;

  private static final Comparator<Pojo> COMPARATOR =
      comparingInt((Pojo p) -> p.id).thenComparing(p -> p.cost);

  @Override
  public int compareTo(@NotNull Pojo pojo) {
    return COMPARATOR.compare(this, pojo);
  }
}

还有一些测试:

import static org.assertj.core.api.Assertions.assertThat;

import java.math.BigDecimal;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class PojoShould {

  private static final int SMALLER = -1;
  private static final int LARGER = 1;
  private static final int EQUAL = 0;

  @ParameterizedTest
  @MethodSource
  void compareToWorks(Pojo p1, Pojo p2, int expectedResult) {
    assertThat(p1.compareTo(p2)).isEqualTo(expectedResult);
  }

  @SuppressWarnings("unused")
  static Stream<Arguments> compareToWorks() {
    return Stream.of(
        Arguments.of(Pojo.builder().id(-1).build(), Pojo.builder().id(0).build(), SMALLER),
        Arguments.of(Pojo.builder().id(0).build(), Pojo.builder().id(-1).build(), LARGER),
        Arguments.of(Pojo.builder().id(0).build(), Pojo.builder().id(0).build(), EQUAL),
        Arguments.of(
            Pojo.builder().id(0).cost(BigDecimal.ZERO).build(),
            Pojo.builder().id(0).cost(BigDecimal.ONE).build(),
            SMALLER),
        Arguments.of(
            Pojo.builder().id(0).cost(BigDecimal.ONE).build(),
            Pojo.builder().id(0).cost(BigDecimal.ZERO).build(),
            LARGER),
        Arguments.of(
            Pojo.builder().id(0).cost(BigDecimal.ZERO).build(),
            Pojo.builder().id(0).cost(BigDecimal.ZERO).build(),
            EQUAL));
  }
}

大多数情况下都有效,但 id 相等的情况下,它会尝试比较 null 成本(当然)。

如何使 Comaparator null 安全?我试过了

  private static final Comparator<Pojo> COMPARATOR =
      nullsFirst(comparingInt((Pojo p) -> p.id))
          .thenComparing(nullsFirst(comparing((p -> p.cost))));

但这于事无补。

这是 Comparator#thenComparing(Function,Comparator) 的文档:

Returns a lexicographic-order comparator with a function that extracts a key to be compared with the given Comparator.

Implementation Requirements:

This default implementation behaves as if thenComparing(comparing(keyExtractor, cmp)).

哪些引用 Comparator#comparing(Function,Comparator):

Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.

The returned comparator is serializable if the specified function and comparator are both serializable.

如文档所述,Comparator 参数用于比较 Function 提取的值。这意味着 that Comparator 必须能够处理 null。这与您似乎想要做的相反。

无需手动编写代码即可实现 null 安全 Comparator 的一种方法是使用 Comparator#nullsFirst(Comparator) or Comparator#nullsLast(Comparator)。这两个方法都包装了另一个 Comparator,如果要比较的两个元素都不是 null,将被调用。基本上,nullsFirstnullsLast 返回的 Comparator 是一个拦截器。

现在只需要 Comparator 将被 null-safe Comparator 包装。由于IntegerBigDecimal都是Comparable,我们可以使用Comparator#naturalOrder().

假设 idcost 都可以是 null,并且您希望 null 值在每种情况下都排在第一位,那么以下内容应该适合您:

comparing(p -> p.id, nullsFirst(naturalOrder()))
        .thenComparing(p -> p.cost, nullsFirst(naturalOrder()));

或者,如果您假设 id 永远不会是 null,您可以像这样修改上面的内容:

comparing(p -> p.id)
        .thenComparing(p -> p.cost, nullsFirst(naturalOrder()));