1. 概述
Java Stream API 引入了许多显著增强代码功能和可读性的特性。其中,map() 方法作为转换集合元素的强大工具尤为突出。常见需求是确保转换结果中不包含 null 元素。
本教程将探讨如何有效收集 Stream 的 map() 方法中的非 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 元素,再执行逗号替换。但为了演示如何从 Stream 的 map() 调用中收集非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
因此可见,由于 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. 总结
本文探讨了如何有效收集 Stream 的 map() 操作中的非 null 元素。同时讨论了为何将映射函数结果包装在 Optional 中会导致效率问题。
一如既往,示例的完整源代码可在 GitHub 获取。