系列中的上一篇
当前教程
使用 Optional
系列中的下一篇

系列中的上一篇: 实现 Collector 接口

系列中的下一篇: 并行化流

使用 Optional

 

支持无法产生结果的方法

我们已经介绍了 Optional 类的几种用法,尤其是在对没有标识元素的流调用终止操作的情况下。这种情况不容易处理,因为你无法返回任何值,包括 0,而返回 null 会导致你的代码必须在你不希望出现的地方处理 null 值。

能够在无法产生值的情况下使用 Optional 类,为更好的模式提供了许多机会,尤其是对于更好地处理错误。这是你应该使用可选对象的主要原因:它们表明方法在某些情况下可能不会产生结果。在字段、列表、映射中存储 Optional 实例,或将其作为方法参数传递,并不是可选对象创建的目的。

如果你设计了一个返回可选对象的方法,或者需要在变量中存储可选对象,那么你不应该返回 null 或将此变量设置为 null。你应该利用可选对象可以为空的事实。

简而言之,Optional 类是一个包装类,可以包装引用:Optional<T>,或值:OptionalIntOptionalLong,以及 OptionalDouble。与你已经知道的经典包装类型(例如:IntegerLongDouble 等)的区别在于,可选对象可以为空。这样的实例不包装任何内容。

如果你需要一种机制从你的方法中返回一些东西,这意味着没有值,而返回 null 会导致错误,包括 NullPointerException,那么你应该考虑返回一个可选对象,并在这种情况下返回一个空的可选对象。

 

创建 Optional 对象

Optional 类是一个具有私有构造函数的最终类。因此,创建它的实例的唯一方法是调用它的一个工厂方法。有三个这样的方法。

  1. 你可以通过调用 Optional.empty() 来创建一个空的可选对象。
  2. 你可以通过调用 Optional.of() 来包装一个非空元素,并将该元素作为参数传递。不允许将 null 引用传递给此方法。在这种情况下,你将得到一个 NullPointerException
  3. 你可以通过调用 Optional.ofNullable() 来包装任何元素,并将该元素作为参数传递。你可以将 null 引用传递给此方法。在这种情况下,你将得到一个空的可选对象。

这些是创建此类实例的唯一方法。如你所见,你无法用可选对象包装 null 引用。结果是,打开一个非空的可选对象将始终返回一个非空引用。

Optional<T> 类有三个等效类,用于与数字的专用流一起使用:OptionalIntOptionalLong,以及 OptionalDouble。这些类是基本类型的包装器,即值。方法 ofNullable() 对这些类没有意义,因为值不能为 null。

 

打开 Optional 对象

有几种方法可以使用可选对象并访问它包装的元素(如果有)。你可以直接查询你拥有的实例并打开它(如果其中有内容),或者你可以在它上面使用类似流的方法:map()flatMap()filter(),甚至 forEach() 的等效方法。

打开可选对象以获取其内容应谨慎进行,因为它会引发 NoSuchElementException(如果可选对象为空)。除非你确定你的可选对象中有一个元素,否则你应该通过先测试它来保护此操作。

有两个方法可以用来测试你的可选对象:isPresent(),以及在 Java SE 11 中添加的 isEmpty()

然后,要打开你的可选对象,你可以使用以下方法。

你也可以尝试获取可选对象的內容,并提供一个将在可选对象为空时返回的对象。

  • orElse(T returnedObject): 如果在空可选对象上调用,则返回参数。
  • orElseGet(Supplier<T> supplier): 与上一个方法的作用相同,但无需构建返回的对象,以防构建此对象证明成本很高。实际上,提供的供应商仅在需要时才会被调用。

最后,你可以在此可选对象为空的情况下创建另一个可选对象。

  • or(Supplier<Optional> supplier): 如果此可选对象不为空,则返回未修改的可选对象;如果为空,则调用提供的供应商。此供应商创建另一个可选对象,该对象由此方法返回。

 

处理 Optional 对象

Optional 类还提供了一些模式,以便您可以将可选对象的处理与流处理集成。它包含与 Stream API 中的方法直接对应的方法,您可以使用这些方法以相同的方式处理数据,并且这些方法将与流无缝集成。这些方法是 map()filter()flatMap()。它们接受与 Stream API 中的同名方法相同的参数,除了 flatMap(),它接受一个返回 Optional<T> 实例的函数,而不是 Stream 实例。

这些方法返回一个可选对象,遵循以下约定。

  1. 如果调用它们的可选对象为空,则它们返回一个可选对象。
  2. 如果它不为空,则将它们的函数或谓词应用于此选项的内容。结果将包装在另一个选项中,该选项由此方法返回。

在某些流模式中,使用这些方法可以使代码更易读。

假设您有一个包含 id 属性的 Customer 实例列表。您需要找到具有给定 ID 的客户的姓名。使用流词汇,您需要找到具有给定 ID 的客户,并将其映射到其姓名。

您可以使用以下模式来实现这一点。

String findCustomerNameById(int id){
    List<Customer> customers = ...;

    return customers.stream()
                    .filter(customer->customer.getId() == id);
                    .findFirst()
                    .map(Customer::getName)
                    .orElse("UNKNOWN");
}

您可以在此模式中看到,map() 方法来自 Optional 类,并且它与流处理很好地集成。您不需要检查 findFirst() 方法返回的可选对象是否为空;实际上,调用 map() 会为您执行此操作。

获取共同发表文章最多的两位作者

让我们检查另一个更复杂的示例,该示例演示了如何使用这些方法。通过这个示例,您将了解 Stream API、Collector API 和可选对象的几个主要模式。

假设您有一组需要处理的文章。文章包含标题、出版年份和作者列表。作者有姓名。

您的列表中有很多文章,您需要知道哪些作者共同发表了最多的文章。

您的第一个想法可能是为给定文章构建一对作者的流。这实际上是给定文章的作者集的笛卡尔积。您不需要此流中的所有对。您对两个作者实际上是同一个作者的对不感兴趣;一对两个作者 (A1, A2) 与对 (A2, A1) 相同。为了实现此约束,您可以通过声明在对中,作者按字母顺序排序,为对添加约束。

让我们为这个模型编写两个记录。

record Article (String title, int inceptionYear, List<Author> authors) {}

record Author(String name) implements Comparable<Author> {

    public int compareTo(Author other) {
        return this.name.compareTo(other.name);
    }
}

record PairOfAuthors(Author first, Author second) {
    
    public static Optional<PairOfAuthors> of(Author first, Author second) {
        if (first.compareTo(second) > 0) {
            return Optional.of(new PairOfAuthors(first, second));
        } else {
            return Optional.empty();
        }
    }
}

PairOfAuthors 记录中创建工厂方法允许您控制允许的记录实例,并防止创建不需要的对。为了表明此工厂方法可能无法生成结果,您可以将其包装在可选对象中。这完全符合原则:如果您无法生成结果,则返回一个空的可选对象。

让我们编写一个函数,该函数为给定文章创建一个 Stream<PairOfAuthors>。您可以使用两个嵌套流创建笛卡尔积。

作为第一步,您可以编写一个双函数,该函数从文章和作者创建此流。

BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors =
    (article, firstAuthor) ->
        article.authors().stream().flatMap(
            secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());

此双函数从 firstAuthorsecondAuthor 创建一个可选对象,这些对象取自基于文章作者构建的流。您可以看到,stream() 方法被调用到 of() 方法返回的可选对象上。如果可选对象为空,则返回的流为空;否则,它只包含一对作者。此流由 flatMap() 方法处理。此方法打开流,空流将消失,只有有效的对将出现在结果流中。

您现在可以构建一个函数,该函数使用此双函数从文章创建一对作者的流。

Function<Article, Stream<PairOfAuthors>> toPairOfAuthors =
    article ->
    article.authors().stream()
                     .flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));

了解共同发表文章最多的两位作者可以通过直方图来完成,直方图中的键是一对作者,值是他们共同撰写的文章数量。

您可以使用 groupingBy() 收集器构建直方图。让我们首先创建一对作者的流。

Stream<PairOfAuthors> pairsOfAuthors =
    articles.stream()
            .flatMap(toPairOfAuthors);

此流的构建方式是,如果一对作者共同撰写了两篇文章,则这对作者将在流中出现两次。因此,您需要做的是计算每对作者在流中出现的次数。这可以通过 groupingBy() 模式来完成,其中分类器是恒等函数:对本身。此时,值是成对的列表,您需要对其进行计数。因此,下游收集器只是 counting() 收集器。

Map<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)
            .collect(Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()
            ));

查找共同发表文章最多的作者包括提取此映射的最大值。您可以为此处理创建以下函数。

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor =
    map -> map.entrySet().stream()
                         .max(Map.Entry.comparingByValue())
                         .orElseThrow();

此函数在 Stream.max() 方法返回的可选对象上调用 orElseThrow() 方法。

此可选对象可能为空吗?为了使其为空,映射本身必须为空,这意味着原始流中没有一对作者。只要您至少有一篇文章由至少两位作者撰写,则此可选对象就不会为空。

获取每年共同发表文章最多的两位作者

让我们更进一步,看看您是否可以对每一年执行相同的处理。实际上,能够使用单个收集器实现此处理将解决您的问题,因为您随后可以将其作为下游收集器传递给 groupingBy(Article::inceptionYear) 收集器。

映射的后处理以提取最大值可以作为 collectingAndThen() 收集器。此模式已在上一节“使用完成器对收集器进行后处理”中介绍过。此收集器如下所示。

让我们提取 groupingBy() 收集器和完成器。如果您使用 IDE 键入此代码,则可以使用它来获取收集器的正确类型。

Collector<PairOfAuthors, ?, Map<PairOfAuthors, Long>> groupingBy =
        Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
        );

Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> finisher =
        map -> map.entrySet().stream()
                  .max(Map.Entry.comparingByValue())
                  .orElseThrow();

现在,您可以将它们合并到一个 collectingAndThen() 收集器中。您可以将 groupingBy() 收集器识别为第一个参数,将 finisher 函数识别为第二个参数。

Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector =
    Collectors.collectingAndThen(
            Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
            ),
            map -> map.entrySet().stream()
                      .max(Map.Entry.comparingByValue())
                      .orElseThrow()
    );

您现在可以使用初始 flatmap 操作和此收集器编写完整的模式。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .flatMap(toPairOfAuthors)
            .collect(pairOfAuthorsEntryCollector);

由于 flatMapping() 收集器,您可以通过合并中间 flatMap() 和终端收集器,使用单个收集器编写此代码。以下代码等效于前面的代码。

Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =
    articles.stream()
            .collect(
                Collectors.flatMapping(
                    toPairOfAuthors,
                    pairOfAuthorsEntryCollector));

查找每年共同发表文章最多的两位作者,只需将此 flatMapping() 收集器作为下游收集器传递给正确的 groupingBy()

Collector<Article, ?, Map.Entry<PairOfAuthors, Long>> flatMapping = 
    Collectors.flatMapping(
            toPairOfAuthors,
            pairOfAuthorsEntryCollector));

Map<Integer, Map.Entry<PairOfAuthors, Long>> result =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                    Article::inceptionYear,
                    flatMapping
                )
            );

您可能还记得,在这个 flatMapping() 收集器的深处,有一个对 Optional.orElseThrow() 的调用。在前面的模式中,检查此调用是否可能失败很容易,因为在这一点上有一个空可选对象很容易猜测。

现在我们已经将此收集器用作下游收集器,情况有所不同。您如何确保每一年都至少有一篇文章由至少两位作者撰写?最好保护此代码免受任何 NoSuchElementException

避免打开可选对象

在第一个上下文中可能可以接受的模式现在变得更加危险。处理它包括首先不调用 orElseThrow()

在这种情况下,收集器变为以下内容。它不再创建一对作者和一个长数字的键值对,而是将结果包装在一个可选对象中。

Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>> 
        pairOfAuthorsEntryCollector =
            Collectors.collectingAndThen(
                Collectors.groupingBy(
                    Function.identity(),
                    Collectors.counting()
                ),
                map -> map.entrySet().stream()
                          .max(Map.Entry.comparingByValue())
            );

请注意,orElseThrow() 现在不再被调用,因此导致收集器签名中的可选对象。

此可选对象也出现在 flatMapping() 收集器的签名中。

Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =
        Collectors.flatMapping(
                toPairOfAuthors,
                pairOfAuthorsEntryCollector
        );

使用此收集器创建每年作者对的映射会创建一个类型为 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>> 的映射,这不是我们需要的类型:拥有一个值为空可选的映射是无用的,而且可能很昂贵。这是一种反模式。不幸的是,在计算此最大值之前,你无法猜测此可选值是否为空。

构建此中间映射后,你需要摆脱空可选值以构建表示所需直方图的映射。我们将使用与之前相同的技术:在可选对象上调用 stream() 方法,并在 flatMap() 中调用,以便 flatMap() 操作会静默地删除空可选值。

模式如下。

Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram =
    articles.stream()
            .collect(
                Collectors.groupingBy(
                        Article::inceptionYear,
                        flatMapping
                )
            )  // Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>
            .entrySet().stream()
            .flatMap(
                entry -> entry.getValue()
                              .map(value -> Map.entry(entry.getKey(), value))
                              .stream())
            .collect(Collectors.toMap(
                    Map.Entry::getKey, Map.Entry::getValue
            )); // Map<Integer, Map.Entry<PairOfAuthors, Long>>

请注意此模式中的 flatmap 函数。它接受一个 entry 作为参数,其值为 Optional<Map.Entry<PairOfAuthors, Long>> 类型,并在此可选值上调用 map()

如果可选值为空,则此调用将返回一个空可选值。然后忽略映射函数。对 stream() 的下一次调用将返回一个空流,该流将从主流中删除,因为我们处于 flatMap() 调用中。

如果可选值中存在值,则将使用此值调用映射函数。此映射函数使用相同的键和此现有值创建一个新的键值对。此键值对的类型为 Map.Entry<PairOfAuthors, Long>,它由此 map() 方法包装在可选对象中。对 stream() 的调用使用此可选值的内容创建一个流,然后由 flatMap() 调用打开。

此模式将具有空可选值的 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>> 映射到 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>,删除所有具有空可选值的键值对。

可以使用 toMap() 收集器安全地重新创建映射,因为你知道在此流中不可能出现两次相同的键。

此模式使用 Stream API 和可选值的三个重要点。

  1. 如果在空可选值上调用 Optional.map() 方法,则该方法将返回一个空可选值。
  2. 如果可选值为空,则 Optional.stream() 方法将在可选值的内容上打开一个流。返回的流也将为空。它允许你无缝地从可选空间移动到流空间。
  3. 如果可选值为空,则 Stream.flatMap() 方法将打开从可选值构建的流,静默地删除空流。

 

使用可选值的内容

Optional 类还有两个方法,它们接受一个消费者作为参数。

 

说明一些正确使用可选值的规则

规则 #1 永远不要对可选变量或返回值使用 null。

规则 #2 除非你确定可选值不为空,否则永远不要调用 orElseThrow()get()

规则 #3 优先使用 ifPresent()orElseThrow()get() 的替代方法。

规则 #4 不要创建可选值以避免测试引用的空值。

规则 #5 不要在字段、方法参数、集合和映射中使用可选值。

规则 #6 不要对可选对象使用身份敏感操作,例如引用相等性、身份哈希码和同步。

规则 #7 不要忘记可选对象不可序列化。


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
使用 Optional
系列中的下一篇

系列中的上一篇: 实现 Collector 接口

系列中的下一篇: 并行化流