Stream 的 flatMap 和 map 有什么区别?什么时候用 flatMap?
简化版
map 和 flatMap都是 Stream 的转换操作,区别在「一对一」还是「一对多再摊平」:map(fn) 是「一对一转换」——每个元素经过 fn 变成一个新元素(List<String> 经过 map(String::length) 变成 List<Integer>,元素个数不变)。flatMap(fn) 是「一对多 + 扁平化」——每个元素经过 fn 变成一个流(多个元素),然后 flatMap 把这些「小流」全部「摊平/合并」成一个大流。典型场景:当你 map 之后得到的是「流的流」(Stream<Stream<T>> 或 Stream<List<T>>)时,就该用 flatMap 把它「压平」成 Stream<T>。最经典的例子:把 List<List<Integer>>(嵌套列表)变成一个 List<Integer>(所有元素平铺)——map 只能得到 Stream<List<Integer>>(还是嵌套的),flatMap 才能得到 Stream<Integer>(平铺的)。一句话记:map 是「变形」(元素→元素),flatMap 是「拆开再合并」(元素→流→摊平)。
详细版
map vs flatMap:
| 维度 | map | flatMap |
|---|---|---|
| 转换 | 一对一(元素 → 元素) | 一对多(元素 → 流) |
| 函数返回 | 一个值 R | 一个 Stream |
| 结果 | Stream | Stream |
| 用途 | 普通转换 | 摊平嵌套结构、一拆多 |
// map:一对一转换(元素个数不变)
List<String> words = List.of("hello", "world");
List<Integer> lengths = words.stream()
.map(String::length) // "hello"→5, "world"→5
.collect(Collectors.toList()); // [5, 5]
// flatMap:摊平嵌套列表
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4), List.of(5));
List<Integer> flat = nested.stream()
.flatMap(List::stream) // 每个 List → Stream,全部摊平
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
// 对比:用 map 只能得到「流的流」
// nested.stream().map(List::stream) → Stream<Stream<Integer>>(还是嵌套)
// flatMap 一拆多:把每句话拆成单词
List<String> sentences = List.of("hello world", "foo bar");
List<String> allWords = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" "))) // 每句 → 多个单词流,摊平
.collect(Collectors.toList()); // [hello, world, foo, bar]
⚠️ 判断该用 map 还是 flatMap,就看「转换函数返回的是一个值还是一个流/集合」:如果
fn返回单个值(String::length返回int),用map;如果fn返回的是一个流或集合(一个元素对应多个结果,如「一个订单 → 它的多个订单项」、「一句话 → 多个单词」),并且你想要「把这些结果平铺在一起」,就用flatMap。新手常见的错误信号是「结果变成了Stream<Stream<X>>、Stream<List<X>>、List<List<X>>这种嵌套」——一旦发现结果多了一层嵌套,往往就是「该用 flatMap 却用了 map」。flatMap 的本质是「map(一对多)+ flatten(摊平一层嵌套)」两步合一。
完整版教学
一、map:一对一转换
先理解 map 的「一对一」:
map(Function<T, R>):
对流的每个元素应用函数,得到一个新元素
元素个数不变(一进一出)
Stream<T> --map(T→R)--> Stream<R>
[a, b, c] --map--> [f(a), f(b), f(c)]
例:
words.map(String::length) // 每个词 → 它的长度
nums.map(n -> n * 2) // 每个数 → 它的两倍
users.map(User::getName) // 每个用户 → 名字
特点:
① 一对一:输入 n 个元素,输出 n 个元素
② 函数返回单个值(不是流/集合)
③ 只是"变形",不改变元素数量
所以 map 用于"普通的转换"——把每个元素换成另一个东西
map(Function<T,R>) 是「一对一转换」——对每个元素应用函数得到一个新元素,元素个数不变([a,b,c]→[f(a),f(b),f(c)])。函数返回单个值(String::length 返回长度)。它只是「变形」不改变数量。用于「普通的转换」(把每个元素换成另一个东西)。理解「map 一对一转换、元素个数不变、函数返回单个值、只是变形」,就掌握了 map。
二、flatMap:一对多 + 摊平
flatMap 是「一对多再摊平」——理解它的两步:
flatMap(Function<T, Stream<R>>):
① map 阶段:每个元素 → 一个流(Stream<R>)
(所以一个元素可能对应多个结果)
② flatten 阶段:把这些"小流"全部摊平、合并成一个大流
Stream<T> --flatMap(T→Stream<R>)--> Stream<R>(摊平的)
图解:
[a, b] --map(每个→流)--> [[a1,a2], [b1,b2,b3]] (流的流)
--flatten摊平--> [a1, a2, b1, b2, b3] (一个流)
例:摊平嵌套列表
[[1,2], [3,4], [5]]
--flatMap(List::stream)--> 每个 List 变成流:[1,2] [3,4] [5]
--摊平--> [1, 2, 3, 4, 5]
例:一拆多
["hello world", "foo bar"]
--flatMap(拆分成单词流)--> ["hello","world"] ["foo","bar"]
--摊平--> ["hello", "world", "foo", "bar"]
关键:flatMap = map(一对多,返回流)+ flatten(去掉一层嵌套)
→ 元素数量可能变多(一个变多个),且结果被"压平"
flatMap(Function<T, Stream<R>>) 是「一对多 + 摊平」,两步:① map 阶段每个元素→一个流(一个元素对应多个结果);② flatten 阶段把这些小流摊平合并成一个大流。图解:[a,b]→[[a1,a2],[b1,b2,b3]](流的流)→[a1,a2,b1,b2,b3](一个流)。关键:flatMap = map(一对多返回流)+ flatten(去掉一层嵌套),元素数量可能变多、结果被压平。理解「flatMap 一对多+摊平两步:map 阶段每元素→流、flatten 阶段摊平成一个流、=map(返回流)+flatten(去一层嵌套)、元素可能变多且压平」,就掌握了 flatMap 的本质。
三、核心区别:返回值决定用哪个
区分 map 和 flatMap 的关键是「转换函数返回什么」:
看转换函数返回"一个值"还是"一个流/集合":
返回单个值 → 用 map
String::length 返回 int(单个值)→ map
返回流/集合(一对多)→ 用 flatMap
List::stream 返回 Stream(多个值)→ flatMap
s -> Arrays.stream(s.split(" ")) 返回流 → flatMap
用错的后果(该 flatMap 却用 map):
nested.stream().map(List::stream)
→ List::stream 返回 Stream<Integer>
→ map 把每个元素变成 Stream → 得到 Stream<Stream<Integer>>(嵌套!)
→ 结果多了一层,还是嵌套的,没达到"摊平"目的
用错的信号:
结果类型出现 Stream<Stream<X>>、Stream<List<X>>、List<List<X>>
→ 多了一层嵌套 → 应该用 flatMap 去掉这层
所以判断:
fn 返回单值 → map
fn 返回流/集合,且想摊平 → flatMap
区分 map 和 flatMap 看「转换函数返回什么」:返回单个值→用 map(String::length 返回 int);返回流/集合(一对多)且想摊平→用 flatMap(List::stream 返回流)。用错的后果(该 flatMap 却用 map):map(List::stream) 得到 Stream<Stream<Integer>>(多了一层嵌套没摊平)。用错的信号:结果出现 Stream<Stream<X>>/List<List<X>> 这种嵌套(该用 flatMap 去掉这层)。理解「区分看转换函数返回什么:返回单值用 map、返回流/集合想摊平用 flatMap、用错信号是结果多一层嵌套(Stream
四、典型应用场景
flatMap 的几个典型应用场景:
场景一:摊平嵌套集合
List<List<T>> → List<T>
nested.stream().flatMap(List::stream).collect(toList())
场景二:一对多展开
一个订单 → 它的多个订单项
orders.stream().flatMap(o -> o.getItems().stream())
→ 所有订单的所有订单项,平铺成一个流
场景三:字符串拆分
多个句子 → 所有单词
sentences.stream().flatMap(s -> Arrays.stream(s.split(" ")))
场景四:Optional 的 flatMap(不产生嵌套 Optional)
user.flatMap(User::getAddress) // 避免 Optional<Optional<Address>>
(Optional 也有 map/flatMap,同样的道理)
场景五:多层数据关联展开
用户 → 用户的订单 → 订单的商品
users.stream()
.flatMap(u -> u.getOrders().stream())
.flatMap(o -> o.getItems().stream())
→ 一层层展开,最终得到所有商品的平铺流
共同点:都是"一个元素展开成多个",然后"平铺在一起"
flatMap 的典型应用:① 摊平嵌套集合(List<List<T>>→List<T>);② 一对多展开(一个订单→多个订单项,所有订单的订单项平铺);③ 字符串拆分(多个句子→所有单词);④ Optional 的 flatMap(避免 Optional<Optional<T>> 嵌套);⑤ 多层数据关联展开(用户→订单→商品,层层 flatMap 展开平铺)。共同点:都是「一个元素展开成多个再平铺」。理解「flatMap 典型场景:摊平嵌套集合/一对多展开(订单→订单项)/字符串拆分/Optional flatMap 避免嵌套/多层关联展开、共同点是一元素展开多个再平铺」,就掌握了 flatMap 的应用。
五、与 reduce 的配合(补充)
顺带理解 reduce(归约)——另一个常见的终端操作:
reduce:把流"归约"成一个值(累积/聚合)
三种形式:
① reduce(BinaryOperator):无初始值,返回 Optional
nums.stream().reduce((a, b) -> a + b) // 求和,返回 Optional
② reduce(identity, BinaryOperator):有初始值
nums.stream().reduce(0, Integer::sum) // 从 0 开始求和,返回 int
③ reduce(identity, accumulator, combiner):并行归约用
(combiner 合并各段的部分结果)
reduce 和 map/flatMap 的关系:
map/flatMap 是中间操作(转换)
reduce 是终端操作(把流归约成一个结果)
常配合:map 转换 → reduce 聚合
orders.stream().map(Order::getAmount).reduce(0, Integer::sum)
→ 先取金额,再求和
reduce vs collect:
reduce:归约成"一个值"(求和、求积、拼接)
collect:归约成"一个容器"(List、Map),更灵活
→ 简单聚合成单值用 reduce,收集成集合用 collect
顺带理解 reduce(归约)——把流归约成一个值。三种形式:reduce(BinaryOperator)(无初始值返回 Optional)、reduce(identity, BinaryOperator)(有初始值返回值)、reduce(identity, accumulator, combiner)(并行归约)。它是终端操作(map 转换 → reduce 聚合)。reduce vs collect:reduce 归约成一个值(求和/求积)、collect 归约成容器(List/Map 更灵活)。理解「reduce 归约成一个值(三形式:无初始值返回 Optional/有初始值/并行)、map 转换+reduce 聚合、reduce 归约成单值 vs collect 归约成容器」,就补充了 reduce 的知识。
六、实践小结
总结 map/flatMap 的实践:
选择:
转换(一对一,元素→元素)→ map
展开(一对多,元素→多个,要平铺)→ flatMap
判断技巧:
转换函数返回单值 → map
转换函数返回流/集合 → flatMap
结果出现嵌套(Stream<Stream>/List<List>)→ 该用 flatMap
常见组合:
filter → map → collect(过滤、转换、收集)
flatMap → distinct → collect(展开、去重、收集)
map → reduce(转换、聚合)
一句话记忆:
map = 变形(每个元素换个样子)
flatMap = 拆开再合并(每个元素拆成多个,全部平铺)
对称理解:
Optional 也有 map/flatMap,Stream 也有——
凡是"容器/包装"类型,map 是"对里面的值变换"、
flatMap 是"对里面的值变换成新容器并摊平"
→ 是函数式编程的通用模式(Functor/Monad)
map/flatMap 的选择:转换(一对一)用 map、展开(一对多要平铺)用 flatMap。判断技巧:转换函数返回单值用 map、返回流/集合用 flatMap、结果出现嵌套该用 flatMap。常见组合:filter→map→collect、flatMap→distinct→collect、map→reduce。记忆:map=变形、flatMap=拆开再合并。对称理解:Optional 也有 map/flatMap——凡是「容器/包装」类型,map 是对里面的值变换、flatMap 是变换成新容器并摊平(函数式的 Functor/Monad 通用模式)。理解「选择:一对一用 map、一对多要平铺用 flatMap;map=变形、flatMap=拆开再合并;Optional 也有 map/flatMap 是容器类型的通用模式」,就掌握了 map/flatMap 的实践和更深的理解。
记忆钩子:「map(fn)一对一转换(元素→一个元素,函数返回单值,个数不变,如 String::length);flatMap(fn)一对多+摊平(元素→一个流,函数返回 Stream,把小流摊平成大流,=map(返回流)+flatten(去一层嵌套),元素可能变多);★判断:转换函数返回单值用 map、返回流/集合想摊平用 flatMap、结果出现嵌套(Stream
/List 。)就该用 flatMap;典型场景:摊平嵌套集合(List
→List)/一对多展开(订单→订单项)/字符串拆句成词/Optional flatMap 避免嵌套/多层关联展开;记忆 map=变形、flatMap=拆开再合并;Optional 也有 map/flatMap 是容器类型通用模式」
七、常见误区与追问
- 误区:map 和 flatMap 差不多,可以互换。 不能——map 是一对一(元素→一个元素,函数返回单值);flatMap 是一对多+摊平(元素→一个流,函数返回 Stream,把小流摊平);该用 flatMap 却用 map 会得到嵌套结果(Stream
),没达到摊平目的。 - 误区:想摊平 List
- 用 map(List::stream) 就行。
>(还是嵌套的);要用 flatMap(List::stream) 才能把这些小流摊平成一个 Stream 。 - 误区:flatMap 不改变元素数量。 flatMap 的元素数量可能变多——每个元素展开成一个流(多个元素),摊平后总数是所有小流元素数之和;如「两句话」flatMap 拆成单词后可能是「五个单词」;map 才是元素个数不变(一对一)。
- 误区:结果出现 Stream
或 List 往往是「该用 flatMap 却用了 map」的信号——map 一对一,如果转换函数返回的是流/集合,就会多出一层嵌套;发现结果多了一层嵌套,改用 flatMap 去掉这层。- 是正常的。
- 追问:怎么判断该用 map 还是 flatMap? 看转换函数返回什么——返回单个值(String::length 返回 int)用 map;返回一个流或集合(一个元素对应多个结果,如 List::stream、字符串拆分成单词流)并且想把这些结果平铺在一起,用 flatMap;如果发现用 map 后结果类型多了一层嵌套(Stream
/List - ),就该用 flatMap。
- 追问:flatMap 的本质是什么? 是「map + flatten」两步合一——先 map:对每个元素应用函数得到一个流(一对多);再 flatten:把这些小流「摊平」合并成一个大流(去掉一层嵌套);所以 flatMap 既做了转换(一对多),又做了扁平化(去嵌套),常用于处理嵌套结构或一对多展开。
- 追问:Optional 也有 map 和 flatMap,它们的区别和 Stream 一样吗? 道理一样——Optional.map(fn) 里 fn 返回普通值(Optional 自动包装结果);Optional.flatMap(fn) 里 fn 本身返回 Optional(避免 Optional<Optional
> 嵌套);比如 user.flatMap(User::getAddress),getAddress 返回 Optional,用 flatMap 得到 Optional 而非 Optional<Optional>;这是「容器类型」(Stream/Optional)的通用模式。
八、加强记忆
map 和 flatMap 都是 Stream 的转换操作,区别在「一对一」还是「一对多再摊平」。map(fn) 是一对一转换——每个元素经过 fn 变成一个新元素(函数返回单值,元素个数不变,如 map(String::length))。flatMap(fn) 是一对多 + 扁平化——两步:① map 阶段每个元素→一个流(函数返回 Stream<R>,一个元素对应多个结果);② flatten 阶段把这些小流摊平合并成一个大流(去掉一层嵌套,元素可能变多)。判断该用哪个看转换函数返回什么:返回单个值→map;返回流/集合(一对多)且想摊平→flatMap;用错的信号是结果出现嵌套(Stream<Stream<X>>/List<List<X>>)——该用 flatMap 去掉这层。典型场景:摊平嵌套集合(List<List<T>>→List<T>)、一对多展开(订单→订单项)、字符串拆句成词、Optional 的 flatMap(避免嵌套)、多层关联展开。记忆:map=变形、flatMap=拆开再合并;Optional 也有 map/flatMap,是「容器类型」的通用模式。(配合终端操作 reduce 把流归约成一个值。)一句话「map 一对一转换(返回单值/个数不变)、flatMap 一对多+摊平(返回流,=map+flatten 去一层嵌套/元素变多);判断:返回单值用 map、返回流/集合想摊平用 flatMap、结果出现嵌套就该用 flatMap;场景摊平嵌套集合/一对多展开;记忆 map=变形 flatMap=拆开再合并」。