1. 概述
Java 8 引入的一项重要新特性是 Stream API。它还附带了一组 Collectors,允许我们调用 Stream.collect()
方法将流中的元素收集到所需的集合中,例如 List
、Set
、Map
等。
本文将探讨 collect()
方法是否可能返回 null
值。
2. 问题引入
“Stream 的 collect()
方法能返回 null
吗?”这个问题包含两层含义:
- ✅ 使用标准收集器时是否必须进行
null
检查? - ✅ 是否有可能让
collect()
方法返回null
?
本文将从这两个角度展开讨论。首先创建一个字符串列表作为输入数据,方便后续演示:
final List<String> LANGUAGES = Arrays.asList("Kotlin", null, null, "Java", "Python", "Rust");
该列表包含 6 个元素,其中两个是 null
。后续我们将基于此列表构建流,并通过单元测试断言验证 collect()
的返回值。
3. 标准库中的 Collectors 不会返回 null
Java Stream API 提供了一套标准收集器。首先分析这些收集器是否可能返回 null
。
3.1. null 元素不会导致 collect() 返回 null
如果流包含 null
元素,**它们会作为 null
值被包含在 collect()
的结果中,而不会导致 collect()
方法本身返回 null
**。通过以下测试验证:
List<String> result = LANGUAGES.stream()
.filter(Objects::isNull)
.collect(toList());
assertNotNull(result);
assertEquals(Arrays.asList(null, null), result);
测试中,我们先用 filter()
筛选出所有 null
元素,然后收集到 List。结果成功包含两个 null
元素。因此,**流中的 null
元素不会导致 collect()
返回 null
**。
3.2. 空流不会导致 collect() 返回 null
**使用标准收集器时,即使流为空,collect()
方法也不会返回 null
**。当流为空时,collect()
会返回一个空的结果容器(如空 List
、空 Map
等),具体取决于使用的收集器。
以三个常用收集器为例验证:
List<String> result = LANGUAGES.stream()
.filter(s -> s != null && s.length() == 1)
.collect(toList());
assertNotNull(result);
assertTrue(result.isEmpty());
Map<Character, String> result2 = LANGUAGES.stream()
.filter(s -> s != null && s.length() == 1)
.collect(toMap(s -> s.charAt(0), Function.identity()));
assertNotNull(result2);
assertTrue(result2.isEmpty());
Map<Character, List<String>> result3 = LANGUAGES.stream()
.filter(s -> s != null && s.length() == 1)
.collect(groupingBy(s -> s.charAt(0)));
assertNotNull(result3);
assertTrue(result3.isEmpty());
测试中,filter(s -> s != null && s.length() == 1)
会返回空流(无元素满足条件)。结果显示,toList()
、toMap()
和 groupingBy()
均未返回 null
,而是生成了空集合。
结论:所有标准收集器都不会返回 null
。
4. 是否能让 Stream.collect() 返回 null?
已知标准收集器不会让 collect()
返回 null
。那么,如果我们希望 Stream.collect()
返回 null
,是否可行?答案是:可以。
4.1. 创建自定义收集器
标准收集器不返回 null
,但**如果我们创建一个返回可空结果的自定义收集器,Stream.collect()
就可能返回 null
**。
Stream API 提供了静态方法 Collector.of()
用于创建自定义收集器,它接受四个参数:
Supplier
函数:提供可变的结果容器accumulator
函数:将元素合并到容器combiner
函数:合并并行流中的中间结果finisher
函数(可选):对容器执行最终转换
关键点:我们可以利用 finisher
函数让收集器返回可空容器。创建一个名为 emptyListToNullCollector
的收集器,其行为类似标准 toList()
,但在结果为空时返回 null
:
Collector<String, ArrayList<String>, ArrayList<String>> emptyListToNullCollector = Collector.of(
ArrayList::new,
ArrayList::add,
(a, b) -> {
a.addAll(b);
return a;
},
a -> a.isEmpty() ? null : a
);
测试该收集器:
List<String> notNullResult = LANGUAGES.stream()
.filter(Objects::isNull)
.collect(emptyListToNullCollector);
assertNotNull(notNullResult);
assertEquals(Arrays.asList(null, null), notNullResult);
List<String> nullResult = LANGUAGES.stream()
.filter(s -> s != null && s.length() == 1)
.collect(emptyListToNullCollector);
assertNull(nullResult);
当流非空时,emptyListToNullCollector
行为与 toList()
一致;当流为空时,返回 null
而非空列表。
4.2. 使用 collectingAndThen() 方法
Stream API 提供了 collectingAndThen()
方法,允许对收集器的结果应用一个结束函数。它接受两个参数:
- 一个收集器(如标准
toList()
) - 一个结束函数:对收集结果执行最终转换
例如,用 collectingAndThen()
创建不可变列表:
List<String> notNullResult = LANGUAGES.stream()
.filter(Objects::nonNull)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
assertNotNull(notNullResult);
assertEquals(Arrays.asList("Kotlin", "Java", "Python", "Rust"), notNullResult);
// 结果列表变为不可变
assertThrows(UnsupportedOperationException.class, () -> notNullResult.add("Oops"));
若仅需扩展标准收集器(如添加结束函数),collectingAndThen()
比自定义收集器更简单。用它实现 emptyListToNullCollector
的功能:
List<String> nullResult = LANGUAGES.stream()
.filter(s -> s != null && s.length() == 1)
.collect(collectingAndThen(toList(), strings -> strings.isEmpty() ? null : strings));
assertNull(nullResult);
通过结束函数,当流为空时 Stream.collect()
返回 null
。
4.3. 关于可空收集器的建议
我们已通过两种方式让收集器返回 null
,但是否应该使用可空收集器需谨慎考虑:
- ⚠️ 避免使用可空收集器,除非有充分理由。
null
值会引入意外行为,降低代码可读性。 - ✅ 如果必须使用,确保下游代码能正确处理
null
值。
正因如此,所有标准收集器均不返回 null
。
5. 总结
本文探讨了 Stream.collect()
是否能返回 null
:
- ✅ 标准收集器永远不会返回
null
- ✅ 可通过
Collector.of()
或collectingAndThen()
让collect()
返回null
- ⚠️ 除非必要,否则应避免使用可空收集器
所有代码片段可在 GitHub 获取。