stream是单次使用的计算过程而非集合,不可重复消费;必须用collect等终端操作落地结果;filter/map/collect为常用三件套;parallelstream需谨慎使用;调试用peek或分步collect暂存。

Stream不是集合,也不能代替集合
很多人一上来就以为 Stream 是个“高级List”,结果调用完 collect() 忘了赋值,或者反复复用同一个流,抛出 IllegalStateException: stream has already been operated upon or closed。它真就只是条流水线——没数据、不存储、只干活、干完就废。
关键区别在于:集合是容器,Stream 是计算过程。你不能对一个 Stream 调两次 forEach,就像不能让同一根水管同时冲两次地。
- 需要多次处理?每次都要重新调用
list.stream() - 想“保存”处理结果?必须用
collect()、toArray()或其他终端操作落地成集合/数组 - 原集合不会被修改——
filter、map都是纯函数式,这点和list.removeIf()有本质区别
filter + map + collect 是最常用三件套
90% 的日常筛选+转换需求,靠这三个中间+终端组合就能搞定。比如从用户列表里取所有25岁以上用户的邮箱:
users.stream()
.filter(u -> u.getAge() > 25)
.map(User::getEmail)
.collect(Collectors.toList());
注意顺序不能乱:filter 要在 map 前,否则空指针风险陡增(比如 u.getEmail() 为 null);collect 必须放在最后,它是唯一真正“启动”整条流水线的操作。
立即学习“Java免费学习笔记(深入)”;
-
filter接收Predicate,返回布尔值;别写成u -> u.getEmail() != null && u.getAge() > 25这种耦合逻辑,拆开更易读、易测试 -
map接收Function,务必确保映射函数不抛异常;若可能为 null,优先用Optional.ofNullable(u.getEmail()).orElse("") -
collect(Collectors.toList())是安全默认项;大数据量且需去重时,改用toSet();要转成 Map,必须处理键冲突(用toMap(k, v, (a,b) -> a))
parallelStream 不是性能银弹
看到“并行”就加 parallelStream()?小心反效果。它只在数据量大(通常 > 10,000)、操作耗时(如 IO、复杂计算)、且无共享状态时才有意义。小列表用它,反而因线程调度开销变慢。
更隐蔽的坑是:并行流下 forEach 不保证顺序,peek 日志会乱序输出;若依赖顺序(比如生成带序号的日志),必须用 forEachOrdered。
- 简单遍历或轻量计算(如
String::length),串行流更快 - 涉及同步资源(如写文件、更新静态计数器),并行流可能引发竞态,得加锁或改用
reduce - 调试阶段一律用
stream(),上线后压测对比再决定是否切并行
别忘了流的生命周期只有一次
这是新手掉进最多次的坑:把流存在字段里反复用,或者在一个方法里调两次终端操作。
比如这个写法会直接报错:
Stream<String> s = list.stream().filter(...); s.forEach(System.out::println); // ✅ 第一次 OK s.count(); // ❌ IllegalStateException
流一旦被消费,内部状态就标记为“已关闭”。没有 reset(),也没有 reopen()。
- 调试时想看中间结果?用
peek(System.out::println),它不终止流 - 需要分步观察?把每一步结果用
collect()暂存为新集合,再基于新集合建新流 - 工具类中若要封装流操作,参数传
List而非Stream,避免调用方传入已消耗流
流的不可复用性不是限制,是设计约束——它强制你把数据处理逻辑显式表达出来,而不是靠隐式状态续命。这点容易忽略,但恰恰是写出可维护代码的起点。









