318. Java Stream API - 深入理解 Java Stream 的中间 Collector —— mapping、filtering 和 fla

318. Java Stream API - 深入理解 Java Stream 的中间 Collector ------ mapping、filtering 和 flatMapping

在使用 Stream 的 collect() 方法时,我们通常接触的 Collector 是 终端操作(terminal operations),比如:

  • Collectors.counting():计算数量 ✅
  • Collectors.joining():拼接字符串 ✅
  • Collectors.toList()、toSet()、toMap():收集到集合 ✅

但除了这些终端操作外,Java 的 Collector API 还提供了一些 中间 Collector,如:

  • mapping
  • filtering
  • flatMapping

这些 Collector 本身无法单独使用,必须配合一个 下游 Collector(downstream collector)。你可以理解为:

🎯 "我们在收集之前,先加工一下再装袋。"


🧭 为什么需要中间 Collector?

有时候,我们不仅想将元素分组或收集,还想在收集之前"处理一下"。比如:

  • 把字符串变成大写再收集
  • 过滤掉不想要的值再分组
  • 将一个对象映射为其某个字段再放进 Map

这时候中间 Collector 就登场了!


🧪 示例 1:使用 mapping() 做数据转换

假设你有一堆英文单词字符串,想将它们变成大写字母并放进列表中:

java 复制代码
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six", 
                                     "seven", "eight", "nine", "ten", "eleven", "twelve");

List<String> result = strings.stream()
    .collect(Collectors.mapping(String::toUpperCase, Collectors.toList()));

System.out.println("result = " + result);

🧾 输出:

java 复制代码
result = [ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, ELEVEN, TWELVE]

解释:

  • mapping(String::toUpperCase, ...) 是中间 Collector,先把元素变为大写。
  • Collectors.toList() 是终端 Collector,把处理结果装进列表。

你可以把这看作是"加工厂 + 装箱厂"的组合 🤖📦。


🧪 示例 2:结合 groupingBy() 和 mapping() 解决复杂 Map 转换问题

假设你要统计不同长度的单词出现次数(即直方图),然后你想倒转这个 Map,以"次数"为 key,得到这些次数对应的"长度"。

第一步:创建直方图 Map<Integer, Long>:

java 复制代码
Map<Integer, Long> histogram = strings.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

第二步:使用 record 将 Map entry 封装成对象,方便处理:

java 复制代码
record NumberOfLength(int length, long number) {
    static NumberOfLength fromEntry(Map.Entry<Integer, Long> entry) {
        return new NumberOfLength(entry.getKey(), entry.getValue());
    }
}

第三步:倒转 Map,以"次数"为 key,用 mapping() 提取 length 字段:

java 复制代码
var map = histogram.entrySet().stream()
    .map(NumberOfLength::fromEntry)
    .collect(Collectors.groupingBy(
        NumberOfLength::number, // 按出现次数分组
        Collectors.mapping(NumberOfLength::length, Collectors.toList()) // 提取 length 放进 List
    ));

map.forEach((key, value) -> System.out.println(key + " :: " + value));

🔍 输出示例:

java 复制代码
2 :: [6]
3 :: [3, 4, 5]

这表示:长度为 3、4、5 的单词各出现了 3 次;长度为 6 的出现了 2 次。


🏆 提取最大值对应的 key(次数最多的字符串长度)

java 复制代码
Map.Entry<Long, List<Integer>> result =
    map.entrySet().stream()
        .max(Map.Entry.comparingByKey()) // 找到次数最多的 group
        .orElseThrow();

System.out.println("result = " + result);

输出:

java 复制代码
result = 3=[3, 4, 5]

你就得到了出现次数最多的字符串长度(3 次):长度为 3、4、5 的单词。


🧪 示例 3:使用 filtering() 收集特定条件的值

  • java9+可用
java 复制代码
Map<Integer, List<String>> filteredGrouped =
    strings.stream()
           .collect(Collectors.groupingBy(
               String::length,
               Collectors.filtering(s -> s.contains("e"), Collectors.toList())
           ));

System.out.println(filteredGrouped);

📌 解释:

  • filtering() 是一个中间 Collector,只收集包含字母 'e' 的单词。
  • 最终结果还是一个分组,但每组只留下符合条件的值。

🧪 示例 4:使用 flatMapping() 将嵌套集合打平

  • java9+可用

如果你有一个 List<List<String>>,想打平成一个大列表,可以这样:

java 复制代码
List<List<String>> nested = List.of(
    List.of("a", "b"),
    List.of("c"),
    List.of("d", "e", "f")
);

List<String> flat = nested.stream()
    .collect(Collectors.flatMapping(List::stream, Collectors.toList()));

System.out.println(flat);

📌 输出:

java 复制代码
[a, b, c, d, e, f]

🎯 总结与培训小贴士

Collector 类型 描述 使用场景
mapping() 将元素映射后再收集 提取字段、格式转换
filtering() 筛选符合条件的元素再收集 条件收集
flatMapping() 扁平化流结构后收集 嵌套集合、多个子项的合并

✅ 这些中间 Collector 是创建嵌套结构和处理 Map 数据的强大工具。 ✅ 多层嵌套也没什么可怕的,本质是"先加工后装袋"的组合模型。 ✅ groupingBy() 搭配中间 Collector 是流处理中最常见也最强大的用法之一。

相关推荐
Dwzun10 分钟前
基于SpringBoot3+Vue3的油莎豆种植管理系统
java·前端·vue.js·spring boot·后端·python·计算机网络
小码哥_常6 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮8 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
学长毕设9 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
她说可以呀10 小时前
Vue插槽
前端·javascript·vue.js
qq_4480111610 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan10 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
2601_9583529011 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
努力的小雨11 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
志尊宝11 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native