Stream 是 Java 里最容易被“写爽”,却最容易在生产翻车”的特性之一。
问题不在 Stream 本身,而在于很多人把它当成:
而真实生产环境里,Stream 有明确的适用边界。
一、先给结论:什么时候该用 Stream?
在生产环境中,我通常只在这三类场景使用 Stream:
集合内存数据处理
无副作用、无状态转换
操作链不超过 4 步
一旦超过这个范围,我会非常谨慎。
二、代码层最佳实践(写得对,比写得短重要)
实践 1:Stream 只做“数据转换”,不做“业务控制”
不推荐写法
orders.stream().forEach(order -> { if(order.isValid()) { try{ process(order); } catch(Exception e) { log.error("error", e); } }});
问题:
判断 + 异常 + 业务混在一起
出问题时无法快速定位
推荐写法
orders.stream() .filter(Order::isValid) // 只负责过滤 .forEach(this::process); // 只负责处理
异常处理放在 process 内部,Stream 只负责流程,不负责兜底。
实践 2:不要为了 Stream 而 Stream
// 不需要 Stream 的场景for (Order order : orders) { if (order.isTimeout()) { handle(order);break; }}
这类代码用 Stream 反而更难写、更难读。
经验结论:
需要 break / continue / return 的地方,大概率不适合 Stream。
实践 3:避免过深的 Stream 链
users.stream() .filter(...) .map(...) .flatMap(...) .sorted(...) .limit(...) .collect(...);
超过 5 步时:
建议:
超过 4 步,考虑拆分中间结果。
三、性能优化核心点(90% 的性能问题在这里)
核心点 1:Stream != 比 for 快
实测结论(单线程):
for 循环:最快
stream:略慢但可接受
parallelStream:大多数场景更慢
因此:
性能敏感路径,优先 for;非关键路径,Stream 更可维护。
核心点 2:避免在 Stream 中创建大量对象
list.stream() .map(item -> newBigDecimal(item)) // 每次都 new .collect(Collectors.toList());
高频场景下,对象创建成本非常明显。
优化方式:
list.stream() .mapToLong(Long::parseLong) .sum();
核心点 3:谨慎使用 parallelStream
不要用在这些场景:
// 典型误用list.parallelStream().forEach(this::save);
parallelStream 使用的是 公共 ForkJoinPool,你无法控制线程数,也无法隔离资源。
什么时候可以用?
list.parallelStream() .map(this::heavyCompute) // CPU 密集 .collect(Collectors.toList());
前提条件:
四、生产级推荐模板
模板 1:安全统计
long count = orders.stream() .filter(Order::isValid) .count();
模板 2:转换 + 收集
List<String> ids = users.stream() .filter(User::isActive) .map(User::getId) .collect(Collectors.toList());
模板 3:分组统计
Map<String, Long> countMap = orders.stream().collect(Collectors.groupingBy(Order::getType,Collectors.counting()));
五、生产环境里的“红线原则”
这是我在项目中坚持的几条硬规则:
Stream 中不写 try-catch
Stream 中不做 IO
Stream 中不修改共享变量
parallelStream 必须压测后才能上