Java常用类库中的Stream操作与Stream API

Java Stream是一次性不可重用的计算管道,消费后抛IllegalStateException;需每次重新创建或转为集合复用,parallelStream慎用于小数据或含I/O场景,collect操作需注意null与线程安全问题。

Java 的 Stream 不是集合,也不是数据结构,它是一次性、不可重用的计算管道——用错一次,后续调用 forEachcollect 就会抛 IllegalStateException: stream has already been operated upon or closed

Stream 一旦消费就关闭,不能重复使用

很多人把 Stream 当成 List 那样反复遍历,结果在第二次 collect()count() 时直接报错。这不是 bug,是设计使然:中间操作(如 filtermap)不执行,终端操作(如 collectfindFirstforEach)才触发流水线并消耗流。

  • 错误写法:
    Stream stream = list.stream().filter(s -> s.length() > 3);
    List a = stream.collect(Collectors.toList());
    List b = stream.collect(Collectors.toList()); // 报 IllegalStateException
  • 正确做法:每次需要新流,就重新调用 list.stream();或先转为集合再复用:List filtered = list.stream().filter(...).collect(Collectors.toList());
  • 如果必须“多次消费”,说明你其实不需要 Stream ——该用 Collection 或数组

parallelStream() 并不总是更快,尤其小数据量或含 I/O

parallelStream() 底层用的是 ForkJoinPool.commonPool(),启动开销、任务拆分、结果合并都有成本。对几千以内元素的简单 CPU 计算,串行反而更稳更快;若操作中含同步块、System.out.println、文件读写等阻塞行为,还可能引发线程竞争或死锁。

  • 适合场景:纯 CPU 密集型、数据量 ≥ 10⁴、无共享状态、无外部依赖的操作(如数值聚合、字符串批量处理)
  • 慎用场景:含 sync

    hronized
    ThreadLocal、数据库连接、日志输出、Random 实例(非线程安全)
  • 可临时切换:用 stream.parallel().sequential() 强制切回串行调试,但注意这不重置流状态

collect() 的三种常用形式与陷阱

collect() 是最易出错的终端操作之一,尤其混淆 Collectors.toList()Collectors.toCollection(ArrayList::new) 的语义差异,或误用 toMap 导致 NullPointerException

  • Collectors.toList() 返回的是不可保证具体类型的 List(JDK 16+ 是 ArrayList,但规范不保证),且不保证线程安全;并发流中用它可能产生未定义行为
  • toMap(keyMapper, valueMapper) 要求 key 不能为 null,否则抛 NullPointerException;遇到重复 key 默认失败,需显式传第三个参数 BinaryOperator 处理冲突(如 (a,b) -> a
  • 想收集到 LinkedHashSet 去重保序?别用 toSet()(不保序),改用:
    stream.collect(Collectors.toCollection(LinkedHashSet::new))

filter + map 组合时 null 值的隐性丢失

map 中返回 null,再接 filter(Objects::nonNull) 看似能兜底,但其实没用——map 本身不会过滤,它只是把元素转成 null,而后续 filter 才负责筛掉这些 null。真正危险的是:某些自定义 map 函数未判空,导致 NullPointerException 直接中断流。

  • 安全写法示例(避免 NPE):
    list.stream()
        .filter(Objects::nonNull)
        .map(s -> s.toUpperCase(Locale.ROOT))
        .filter(Objects::nonNull)
  • 更推荐提前防御:用 Optional 包装映射逻辑,或用 mapMulti(JDK 16+)替代复杂转换
  • 注意 flatMap 返回空 Stream.empty() 是合法的,等价于“跳过该元素”,不是 null

Stream 的核心价值不在语法糖,而在声明式表达“做什么”,而非“怎么做”。但它的不可变性、一次性、延迟执行特性,和集合类完全不同。写完一段 Stream 流水线,先问自己:这个流会不会被意外复用?里面有没有隐藏的副作用?终端操作是否真的只执行一次?这些问题比学会十个 Collectors 更关键。