4、数据流转换算子:Map、Filter、FlatMap、Reduce、Aggregate 算子详解与实战
数据流转换算子,说白了就是你在管道里塞进去的那些「加工机器」。每个算子负责一件事:映射、过滤、展平、聚合。我刚开始接触数据流编程时,总觉得这些概念差不多,直到有一次线上任务因为用错了算子导致 OOM……嗯,从那以后我再也不敢小看它们了。
这一章,我们逐个拆解五个核心算子。每个我都会给出定义、代码示例,以及我在项目中踩过的坑。
4.1 Map:一对一转换
Map 是最基础的转换算子。它接收一个输入元素,输出一个转换后的元素。比例永远是 1:1。
核心特征:输入 N 个元素,输出 N 个元素。元素个数不变,内容改变。
举个例子,从用户实体中提取用户名:
// Java Stream 示例
List<User> users = getUsers();
List<String> names = users.stream()
.map(user -> user.getName())
.collect(Collectors.toList());
我个人习惯把 Map 用在「数据清洗」阶段。比如把字符串转成整数、把对象转成 DTO、把时间戳格式统一。你想想看,如果不用 Map,你得写多少 for 循环?
小技巧:Map 里不要做 IO 操作(比如调用数据库或远程 API)。如果必须做,请使用 flatMap 配合异步调用,否则会阻塞整个流。
4.2 Filter:条件筛选
Filter 负责「留还是不留」。它接收一个谓词(Predicate),返回 true 的留下,false 的丢弃。
比例是 N : M,其中 M ≤ N。
// 筛选出年龄大于 18 的用户
List<User> adults = users.stream()
.filter(user -> user.getAge() >= 18)
.collect(Collectors.toList());
我曾经在 Filter 里写过复杂的业务逻辑,结果调试了整整一个下午。后来我总结了一条铁律:Filter 里的谓词必须是无副作用的纯函数。什么意思?就是不能修改外部变量,不能写日志,不能计数。只做一件事:判断。
避坑指南:Filter 不要和 null 检查混在一起。如果流里有 null 元素,先 filter(Objects::nonNull) 再处理业务逻辑。我曾经因为没做这个检查,线上 NPE 报警响了一整夜。
4.3 FlatMap:一对多展平
FlatMap 是 Map 的升级版。它把一个元素变成零个、一个或多个元素。比例是 1 : N。
你想想看,如果每个用户有多条订单记录,你想把所有订单合并成一个流,Map 做不到——Map 只能返回一个元素。这时候 FlatMap 就派上用场了。
// 将每个用户的订单列表展平为订单流
List<Order> allOrders = users.stream()
.flatMap(user -> user.getOrders().stream())
.collect(Collectors.toList());
FlatMap 的典型场景包括:
- 拆分字符串("a,b,c" → "a", "b", "c")
- 展开嵌套集合
- 处理 Optional 值(flatMap 可以避免多层 Optional 嵌套)
- 异步调用的结果合并
关键区别:Map 返回的是「元素」,FlatMap 返回的是「元素的流」。如果你在 Map 里返回了一个集合,你会得到 List<List<T>>;而 FlatMap 会帮你展平成 List<T>。
4.4 Reduce:归约聚合
Reduce 把整个流「折叠」成一个值。它需要两个参数:初始值(identity)和累加器(accumulator)。
比例是 N : 1。
// 计算所有订单的总金额
BigDecimal total = orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Reduce 有三个重载版本:
| 版本 | 参数 | 返回值 | 适用场景 |
|---|---|---|---|
| 1 | identity, accumulator | T(非空) | 有默认值的聚合 |
| 2 | accumulator | Optional<T> | 流可能为空 |
| 3 | identity, accumulator, combiner | U(类型转换) | 并行流 + 类型转换 |
我记得有一次做实时交易统计,用 Reduce 做累加,结果发现并行流下结果不对。后来才意识到:Reduce 的累加器必须满足结合律。加法、乘法、最大值、最小值都没问题,但字符串拼接就不行——因为拼接不满足结合律。
注意:Reduce 的 identity 值必须满足「恒等」性质。比如加法用 0,乘法用 1。如果你用 1 做加法的初始值,结果会多出 N 个 1,这个 bug 非常隐蔽。
4.5 Aggregate:分组聚合
Aggregate 在 Java Stream 里通常对应 Collectors.groupingBy 和 Collectors.toMap。它把流中的元素按某个键分组,然后对每组进行聚合操作。
比例是 N : M,其中 M 是分组数。
// 按用户 ID 分组,统计每个用户的订单总金额
Map<Long, BigDecimal> userTotal = orders.stream()
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.mapping(
Order::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
)
));
Aggregate 的常见组合:
- groupingBy + counting:统计每个分组的元素个数
- groupingBy + summingInt:求和
- groupingBy + mapping + toList:收集每个分组下的字段列表
- partitioningBy:按布尔条件分成两组
实战建议:如果分组后的聚合逻辑很复杂,不要硬写在一行。先 groupBy 得到 Map,再对每个 entry 做二次处理。代码可读性比「一行流」重要得多。
4.6 算子对比与选型指南
为了方便你快速决策,我整理了一张对比表:
| 算子 | 输入:输出 | 核心作用 | 典型场景 | 常见坑 |
|---|---|---|---|---|
| Map | 1:1 | 类型转换、字段提取 | DTO 转换、格式化 | Map 里做 IO |
| Filter | N:M (M≤N) | 条件筛选 | 数据过滤、空值排除 | 谓词有副作用 |
| FlatMap | 1:N | 展平嵌套结构 | 集合展开、Optional 处理 | 忘记 stream() |
| Reduce | N:1 | 归约聚合 | 求和、求积、拼接 | 不满足结合律 |
| Aggregate | N:M | 分组聚合 | 分组统计、多维汇总 | 嵌套过深 |
4.7 算子组合实战:订单统计系统
光说不练假把式。我们来看一个完整的实战案例。
需求:从订单列表中,统计每个用户最近 30 天的有效订单总金额,并按金额降序排列。
// 1. 过滤:只保留最近 30 天且状态为 PAID 的订单
// 2. 分组:按用户 ID 分组
// 3. 聚合:计算每个用户的订单总金额
// 4. 排序:按金额降序
// 5. 收集:返回排序后的结果
LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
List<UserOrderSummary> result = orders.stream()
.filter(o -> o.getStatus() == OrderStatus.PAID)
.filter(o -> o.getCreateDate().isAfter(thirtyDaysAgo))
.collect(Collectors.groupingBy(
Order::getUserId,
Collectors.mapping(
Order::getAmount,
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
)
))
.entrySet().stream()
.map(e -> new UserOrderSummary(e.getKey(), e.getValue()))
.sorted(Comparator.comparing(UserOrderSummary::getTotalAmount).reversed())
.collect(Collectors.toList());
你看,这个例子一口气用到了 Filter、Map、Aggregate(groupingBy)、Reduce(reducing)、Map(entry 转对象)、排序。每个算子各司其职,组合起来就是一个完整的数据管道。
核心原则:数据流编程的本质就是「把复杂业务拆成多个简单算子的组合」。每个算子只做一件事,但组合起来就能解决 90% 的数据处理问题。
4.8 本章知识体系图
下面这张 SVG 图展示了五个算子的核心关系和数据流向:
从图中你可以看到:Map、Filter、FlatMap 是「串联」的流水线算子,每个元素依次经过;而 Reduce 和 Aggregate 是「终端」算子,它们把流收拢成一个或一组结果。
嗯,这一章的内容就到这里。五个算子,五种思维模式。你不需要一次性记住所有细节,但一定要理解它们的「输入输出比例」和「适用场景」。下次写数据流代码时,先问自己三个问题:
- 元素个数变不变?→ 不变用 Map,变少用 Filter,变多用 FlatMap
- 我要一个结果还是一组结果?→ 一个用 Reduce,一组用 Aggregate
- 我的操作有副作用吗?→ 有副作用就别用 Stream,老老实实写 for 循环
记住这三条,你就能避开 80% 的坑。