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,组合使用威力大

从图中你可以看到:Map、Filter、FlatMap 是「串联」的流水线算子,每个元素依次经过;而 Reduce 和 Aggregate 是「终端」算子,它们把流收拢成一个或一组结果。

嗯,这一章的内容就到这里。五个算子,五种思维模式。你不需要一次性记住所有细节,但一定要理解它们的「输入输出比例」和「适用场景」。下次写数据流代码时,先问自己三个问题:

  • 元素个数变不变?→ 不变用 Map,变少用 Filter,变多用 FlatMap
  • 我要一个结果还是一组结果?→ 一个用 Reduce,一组用 Aggregate
  • 我的操作有副作用吗?→ 有副作用就别用 Stream,老老实实写 for 循环

记住这三条,你就能避开 80% 的坑。