Stream 有哪些使用要点?并行流有什么坑?
结论先行:Stream 让集合处理变得声明式、可链式:中间操作(filter/map/sorted 等)是惰性的,只有遇到终止操作(collect/forEach/count 等)才会真正执行;sorted/distinct 等有状态操作会缓存全部元素,注意内存与开销。并行流(parallelStream)默认跑在全局共享的 ForkJoinPool(线程数约等于 CPU 核数),只有“计算密集 + 数据量大 + 无共享可变状态”时才可能获益;否则共享状态竞争、顺序丢失、IO 任务占满公共池等坑会放大问题。
使用要点清单
- 善用短路:findFirst、anyMatch、limit 等能提前终止流水线,别全量算完再取;
- 避免“多次遍历分别聚合”,可用 reduce 或自定义 Collector 一趟完成;
- 警惕 null:map 返回 null 流到下游可能 NPE,先用 filter(Objects::nonNull) 或 Optional 兜底;
- 不为用而用:小集合、复杂循环体用普通 for 更可读,Stream 不等于性能更好。
并行流坑示例
// 坑 1:并行流 + 共享可变容器 → 线程安全与顺序问题
List<Integer> out = new ArrayList<>();
IntStream.range(0, 100_000).parallel()
.forEach(out::add); // ArrayList 非线程安全,可能丢元素甚至越界
// 正确姿势:用线程安全的收集归约
List<Integer> ok = IntStream.range(0, 100_000).parallel()
.boxed().collect(Collectors.toList());
// 坑 2:把 IO/阻塞任务丢进并行流 → 公共 ForkJoinPool 被占满
list.parallelStream().forEach(url -> fetch(url)); // 慎用,阻塞会拖垮全局并行任务
中间操作与终止操作
- 中间操作(filter/map/sorted/distinct)返回新 Stream,惰性记录、暂不执行;
- 终止操作(collect/forEach/reduce/count)触发整条流水线执行;
- 一条流水线只能有一个终止操作,执行完的 Stream 不可复用;
- distinct/sorted 等有状态操作需要缓冲数据,处理超大集合时注意内存占用。
并行流异常排查手法
先单线程跑一遍对拍 → 若并行结果不一致,重点查共享可变状态
→ 再用 jstack 观察公共 ForkJoinPool 是否被阻塞任务占满
常见追问 / 记忆点
- 追问:并行流线程数能调吗?答:公共池默认 CPU 核数,可通过系统属性 ForkJoinPool.common.parallelism 调整,或自定义 ForkJoinPool 提交任务。
- 追问:什么场景下并行流反而更慢?答:数据量小、任务本身轻、有锁竞争或 IO 阻塞时,切分与调度开销会超过收益。
- 记忆点:惰性求值 + 短路是性能关键;parallelStream 共享全局线程池,IO 型任务慎用。