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

Java 的 Stream 不是集合,也不是数据结构,它是一次性、不可重用的计算管道——用错一次,后续调用 forEach、collect 就会抛 IllegalStateException: stream has already been operated upon or closed。
Stream 一旦消费就关闭,不能重复使用
很多人把 Stream 当成 List 那样反复遍历,结果在第二次 collect() 或 count() 时直接报错。这不是 bug,是设计使然:中间操作(如 filter、map)不执行,终端操作(如 collect、findFirst、forEach)才触发流水线并消耗流。
- 错误写法:
Stream
stream = list.stream().filter(s -> s.length() > 3); List a = stream.collect(Collectors.toList()); List b = stream.collect(Collectors.toList()); // 报 IllegalStateException - 正确做法:每次需要新流,就重新调用
list.stream();或先转为集合再复用:Listfiltered = list.stream().filter(...).collect(Collectors.toList()); - 如果必须“多次消费”,说明你其实不需要
Stream——该用Collection或数组
parallelStream() 并不总是更快,尤其小数据量或含 I/O
parallelStream() 底层用的是 ForkJoinPool.commonPool(),启动开销、任务拆分、结果合并都有成本。对几千以内元素的简单 CPU 计算,串行反而更稳更快;若操作中含同步块、System.out.println、文件读写等阻塞行为,还可能引发线程竞争或死锁。
- 适合场景:纯 CPU 密集型、数据量 ≥ 10⁴、无共享状态、无外部依赖的操作(如数值聚合、字符串批量处理)
- 慎用场景:含
synchronized、ThreadLocal、数据库连接、日志输出、Random实例(非线程安全) - 可临时切换:用
stream.parallel().sequential()强制切回串行调试,但注意这不重置流状态
collect() 的三种常用形式与陷阱
collect() 是最易出错的终端操作之一,尤其混淆 Collectors.toList() 和 Collectors.toCollection(ArrayList::new) 的语义差异,或误用 toMap 导致 NullPointerException。
立即学习“Java免费学习笔记(深入)”;
-
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 更关键。










