1. 概述

Java 8 引入的一项重要新特性是 Stream API。它还附带了一组 Collectors,允许我们调用 Stream.collect() 方法将流中的元素收集到所需的集合中,例如 ListSetMap 等。

本文将探讨 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 获取。


原始标题:Can Stream.collect() Return the null Value?