← 返回题目列表

Stream 的 flatMap 和 map 有什么区别?什么时候用 flatMap?

高频 中等 第 11 / 24 题 更新于 2026/08/03
flatMapmapStream扁平化

简化版

mapflatMap都是 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

维度mapflatMap
转换一对一(元素 → 元素)一对多(元素 → 流)
函数返回一个值 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 看「转换函数返回什么」:返回单个值→用 mapString::length 返回 int);返回流/集合(一对多)且想摊平→用 flatMapList::stream 返回流)。用错的后果(该 flatMap 却用 map):map(List::stream) 得到 Stream<Stream<Integer>>(多了一层嵌套没摊平)。用错的信号:结果出现 Stream<Stream<X>>/List<List<X>> 这种嵌套(该用 flatMap 去掉这层)。理解「区分看转换函数返回什么:返回单值用 map、返回流/集合想摊平用 flatMap、用错信号是结果多一层嵌套(Stream)」,就掌握了 map/flatMap 的选择关键。

四、典型应用场景

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→collectflatMap→distinct→collectmap→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) 就行。 不行——List::stream 返回 Stream,map 会把每个元素变成 Stream,得到 Stream<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)的通用模式。

八、加强记忆

mapflatMap 都是 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=拆开再合并」。