1. 概述

Java Stream API 引入了许多显著增强代码功能和可读性的特性。其中,map() 方法作为转换集合元素的强大工具尤为突出。常见需求是确保转换结果中不包含 null 元素。

本教程将探讨如何有效收集 Streammap() 方法中的非 null 元素。

2. 问题引入

map() 方法为处理元素序列提供了高级抽象。它是一个中间操作,Stream 中的每个元素应用映射函数,生成包含转换后元素的新 Stream

有时映射函数可能返回 null。但我们希望从转换结果中排除这些 null 值。例如,假设我们有一个 String 列表:

static final List<String> INPUT = List.of("f o o", "a,b,c,d", "b a r", "w,x,y,z", "w,o,w");

我们想用以下映射函数转换 INPUT 中的 String 元素:

String commaToSpace(String input) {
    if (input.contains(",")) {
        return input.replaceAll(",", " ");
    } else {
        return null;
    }
}

如你所见,commaToSpace() 方法简单地将 input String 中的所有逗号替换为空格并返回结果。但如果 input 不包含逗号,该方法返回 null

现在我们想使用 commaToSpace() 转换 INPUT确保结果中不包含 null。因此预期结果如下:

static final List<String> EXPECTED = List.of("a b c d", "w x y z", "w o w");

可以看到,INPUT List 有5个元素,但 EXPECTED List 只有3个。

值得说明的是,实际开发中我们可能会采用更直接的方式处理此任务。例如先过滤掉不含逗号的 String 元素,再执行逗号替换。但为了演示如何从 Streammap() 调用中收集非null元素,我们将使用 commaToSpace() 作为映射函数并通过 Stream.map() 调用它。

接下来看看如何使用Stream API和 map() 方法实现。

3. 使用 map() + filter() 组合方案

我们提到 map() 方法应用映射函数(此处为 *commaToSpace()*)对 Stream 中的每个元素完成转换。

映射函数接收一个输入并产生一个转换输出,而 map() 方法本身不执行任何过滤操作。因此 map() 生成的 Stream 大小始终与原始 Stream 相同。换句话说,如果映射函数返回 null,这些 null 值会出现在转换后的 Stream 中。不过我们可以结合使用 filter() 方法从结果 Stream 中移除 null 元素。

下面通过测试代码展示具体实现:

List<String> result = INPUT.stream()
  .map(str -> commaToSpace(str))
  .filter(Objects::nonNull)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

上述代码中,我们使用 filter() 方法配合 Objects::nonNull 方法引用 移除结果 Stream 中的所有 null 元素。

4. 用 Optional 处理 null 值可行吗?

处理 null 值时,有人可能会考虑使用 Optional 类,它专为处理可选值而设计,避免显式使用 null

List<String> result = INPUT.stream()
  .map(str -> Optional.ofNullable(commaToSpace(str)))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

如上例所示,我们首先**将可空值包装进 Optional 对象,得到 *Stream<Optional>**。然后通过 filter() 移除 Stream 中所有空的 Optional。最后,为获取 Stream<Optional> 中持有的 String 值,**需要额外步骤通过 map(Optional::get) 提取值*。

因此可见,由于 Stream 中元素不必要的包装和解包操作,Optional 方案在此场景下效率低下

5. 当映射函数返回 Optional 时怎么办?

我们已讨论过用 Optional 处理 null 元素在此问题中效率不高。但有些场景下映射函数返回的是 Optional 对象而非可空结果,例如:

Optional<String> commaToSpaceOptional(String input) {
    if (input.contains(",")) {
        return Optional.of(input.replaceAll(",", " "));
    } else {
        return Optional.empty();
    }
}

这种情况下,我们可以使用 Optional.orElse(null) 从映射函数返回的 Optional 中提取元素值。这允许我们在 map() 方法内部将空的 Optional 转换为 null 元素

List<String> result = INPUT.stream()
  .map(str -> commaToSpaceOptional(str).orElse(null))
  .filter(Objects::nonNull)
  .collect(Collectors.toList());
 
assertEquals(EXPECTED, result);

代码显示,map() 方法在此执行了两个任务

  • 使用映射函数转换 Stream
  • 解包每个转换后的 Optional 对象

其余步骤与“*map() + filter()*”方案相同。

6. 总结

本文探讨了如何有效收集 Streammap() 操作中的非 null 元素。同时讨论了为何将映射函数结果包装在 Optional 中会导致效率问题。

一如既往,示例的完整源代码可在 GitHub 获取。


原始标题:Return Non-null Elements From Java 8 Map Operation