1. 引言
Java 8 引入的 Stream API 让集合处理变得优雅而富有表达力,filter、map、reduce 等操作让代码更接近业务语义。然而,Stream 并非万能银弹------用错了场景,它可能比传统 for 循环更慢、更难调试,甚至埋下难以察觉的 Bug。
本文不打算重复官方文档,而是聚焦实践中最容易踩的坑:并行流何时反而更慢、装箱带来的隐性开销、短路与副作用问题、Collectors 的陷阱,以及如何用 Stream 优雅地完成订单统计、分组和 Top N 等真实业务需求。
2. filter / map / flatMap / reduce 的正确用法
2.1 filter:先过滤,再处理
filter 用于按条件筛选元素,应尽量放在流水线靠前的位置,让后续操作处理更少的数据。
java
// 推荐:先过滤,减少后续 map 的开销
List<Order> paidOrders = orders.stream()
.filter(o -> o.getStatus() == OrderStatus.PAID)
.map(Order::getAmount)
.collect(Collectors.toList());
2.2 map:一对一转换
map 将每个元素映射为另一个对象。注意:map 不会改变元素个数,只是类型或值的转换。
java
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
2.3 flatMap:一对多展平
flatMap 用于将每个元素映射为多个元素,再展平为一个流。典型场景:订单包含多个商品条目。
java
// 每个订单有多个商品行,统计所有商品
List<Product> allProducts = orders.stream()
.flatMap(order -> order.getItems().stream())
.collect(Collectors.toList());
2.4 reduce:聚合归约
reduce 将流中元素反复组合,得到一个最终结果。适合求和、求积、拼接等场景。
java
// 订单总金额
BigDecimal total = orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
注意 :
reduce的初始值(identity)必须满足结合律,否则并行流下结果可能不一致。
3. 并行流:什么时候更慢?
3.1 并行流的工作原理
并行流通过 parallel() 或 parallelStream() 启用,底层使用 Fork/Join 框架将任务拆分到多个线程执行。但拆分、调度、合并都有额外开销。
3.2 并行流更慢的典型场景
| 场景 | 原因 |
|---|---|
| 数据量很小(如 < 1 万元素) | 拆分与合并开销 > 并行收益 |
| 元素处理极快(如简单加法) | 线程调度开销占比过高 |
| 共享可变状态 | 线程竞争导致性能下降甚至结果错误 |
| 非线程安全的收集器 | Collectors.toList() 本身安全,但自定义收集器需谨慎 |
java
// 数据量小,并行反而更慢
long sum = IntStream.rangeClosed(1, 1000)
.parallel()
.sum(); // 不推荐
3.3 何时适合并行
- 数据量大(通常 > 10 万);
- 元素处理耗时(如远程调用、复杂计算);
- 操作无状态、无共享可变数据。
java
// 适合并行:大量元素 + 耗时计算
List<BigDecimal> results = bigList.parallelStream()
.map(this::expensiveComputation)
.collect(Collectors.toList());
经验法则:不确定时先用顺序流,用基准测试(JMH)验证后再决定是否并行。
4. 装箱、短路、副作用
4.1 装箱开销
Stream<Integer> 中每个元素都是包装类型,涉及自动装箱/拆箱,带来额外内存与 CPU 开销。优先使用原始类型特化流:
java
// 避免装箱
int sum = IntStream.rangeClosed(1, 1_000_000)
.sum();
// 避免:Stream<Integer> 装箱开销大
int badSum = Stream.iterate(1, n -> n + 1)
.limit(1_000_000)
.mapToInt(Integer::intValue)
.sum();
4.2 短路操作
limit、findFirst、anyMatch 等短路操作能提前终止流水线,应尽量放在靠前位置,避免无谓计算。
java
// 找到第一个符合条件的订单即可,无需处理全部
Optional<Order> first = orders.stream()
.filter(o -> o.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0)
.findFirst();
4.3 副作用:不要在流中修改外部状态
Stream 规范要求操作无副作用 。在 forEach 或 peek 中修改外部集合,不仅违反规范,并行流下还会产生竞态条件。
java
// 错误:并行流下线程不安全
List<String> result = new ArrayList<>();
list.parallelStream()
.forEach(s -> result.add(s)); // 竞态!
// 正确:用 collect 收集
List<String> result = list.stream()
.collect(Collectors.toList());
5. Collectors 陷阱
5.1 toMap 的键冲突
Collectors.toMap 遇到重复键会抛出 IllegalStateException,需提供合并函数:
java
// 键冲突时保留旧值
Map<String, Order> byId = orders.stream()
.collect(Collectors.toMap(
Order::getId,
Function.identity(),
(oldVal, newVal) -> oldVal));
5.2 groupingBy 的默认值
groupingBy 默认返回 HashMap,不保证顺序。需要有序结果时用 LinkedHashMap:
java
Map<String, List<Order>> byStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::getStatus,
LinkedHashMap::new,
Collectors.toList()));
5.3 空流与 null 值
Collectors.toList() 对空流返回空列表,但 Collectors.toMap 遇到 null 值会抛 NullPointerException。处理前先判空或过滤。
6. 实战:用 Stream 做订单统计、分组、Top N
6.1 订单统计
java
// 订单总金额
BigDecimal totalAmount = orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 订单数量
long count = orders.stream().count();
// 平均金额
double avg = orders.stream()
.mapToDouble(o -> o.getAmount().doubleValue())
.average()
.orElse(0.0);
6.2 按状态分组
java
Map<OrderStatus, List<Order>> byStatus = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus));
// 分组后统计每组金额
Map<OrderStatus, BigDecimal> sumByStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::getStatus,
Collectors.mapping(
Order::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
6.3 Top N
java
// 金额最高的 5 笔订单
List<Order> top5 = orders.stream()
.sorted(Comparator.comparing(Order::getAmount).reversed())
.limit(5)
.collect(Collectors.toList());
// 按状态分组后,每组取金额 Top 3
Map<OrderStatus, List<Order>> top3ByStatus = orders.stream()
.collect(Collectors.groupingBy(
Order::getStatus,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparing(Order::getAmount).reversed())
.limit(3)
.collect(Collectors.toList()))));
7. 总结
Stream API 是强大的工具,但不是银弹。核心建议:
- 先过滤、后处理,善用短路操作;
- 优先原始类型流,避免装箱开销;
- 避免副作用 ,用
collect而非外部集合; - 谨慎使用并行流,数据量大且计算耗时才值得;
- 留意 Collectors 陷阱:键冲突、null、顺序问题。
掌握这些原则,Stream 才能成为你手中真正趁手的利器,而不是埋雷的暗坑。