Java 8中的map()和flatMap()方法有什么区别?

967

72
类型签名已经说明了一切。map :: Stream T -> (T -> R) -> Stream RflatMap :: Stream T -> (T -> Stream R) -> Stream R - Chris Martin
158
就我所知,那些类型签名甚至看起来都不像Java。(我知道我知道,但说这话会假定许多关于新的改进版“Java++”的知识,因此不能用它来说明map/flatMap的全部情况。) - michael
19
@michael 那个类型签名看起来像是 Haskell,而不是 Java。但实际的 Java 签名是否更易读并不清楚:<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper) - Stuart Marks
17
是的,我所说的“实际Java”是指现代Java。 类似于C ++,现代Java与在90年代开始使用它的人(像我一样,两种语言都用过)几乎是不可识别的。 我只是回复评论,方法签名很难讲述“整个故事”,至少现在是这样,除非有额外的解释(或者对于那位评论者来说,翻译)。 - michael
5
换句话说,map方法的映射函数返回类型为R,而flatMap方法的映射函数返回类型为Stream<R>flatMap方法返回的流是将所有映射后的Stream<R>连接起来的结果。两种方法都返回类型为Stream<R>的流,区别在于映射函数的返回值类型,一个是R,一个是Stream<R> - derekm
2
如果每个元素被转换为单个值,则使用 map。如果每个元素将被转换为多个值,并且需要展平结果流,则使用 flatMap - DimaSan
22个回答

1043
mapflatMap都可以应用于Stream<T>,它们都返回一个Stream<R>。区别在于map操作为每个输入值产生一个输出值,而flatMap操作为每个输入值产生一个任意数量(零个或更多)的值。

这体现在每个操作的参数上。

map操作接受一个Function,该函数对输入流中的每个值进行调用,并产生一个结果值,该结果值被发送到输出流。

flatMap操作接受一个希望消耗一个值并产生任意数量的值概念上的函数。然而,在Java中,方法要返回任意数量的值很麻烦,因为方法只能返回零个或一个值。可以想象一下,如果mapper函数为flatMap接受一个值并返回一个值数组或List,这些值将被发送到输出。考虑到这是streams库,表示任意数量的返回值的一种特别适合的方式是让mapper函数本身返回一个stream!来自mapper返回的stream的值从stream中排出并传递到输出stream中。每次调用mapper函数返回的值“成群地”不会在输出stream中有任何区别,因此输出被称为已“平坦化”。

通常使用flatMap的mapper函数,如果要发送零个值,则返回Stream.empty(),如果要返回多个值,则类似于Stream.of(a, b, c)。但当然可以返回任何stream。


42
在我看来,“flatMap”操作恰恰相反于“flat”。这又一次证明了计算机科学家喜欢将术语搞得混淆不清。就像一个函数被称为“透明”,意味着你看不到它做了什么,只能看到结果;然而在口语中,如果你希望一个过程是“透明的”,则意味着你想要看到它的每个部分。 - coladict
59
尝试从不同的角度看待它:它不是一个透明的盒子,可以透过它看到内部工作情况,而是整个功能本身是透明的,即对你来说是看不见的——但仍然能够正常工作,让你看到你正在使用的内容。在这种情况下,“平坦”是指“嵌套”的相反,flatmap通过展开操作去除了一个嵌套层级。 - Zefiro
13
“透明这个事情”已经困扰了我多年。很高兴知道至少还有一个人有同样的感受。 - Ashok Bijoy Debnath
11
把二级结构变成单级结构就是所谓的“扁平化”,可以参考 Dici 在 Stack Overflow 上的回答,里面有一个例子。https://dev59.com/L18d5IYBdhLWcg3wkS0V#26684582 - andrzej.szmukala
47
这是关于 flatMap 最好的解释。这就是使一切都变得清晰的原因:映射器返回的流中的值被从流中排除,并传递到输出流中。每次对映射器函数的调用返回的值“聚合”在一起,在输出流中不做任何区分,因此输出被称为“扁平化”。谢谢! - neevek
显示剩余7条评论

818

Stream.flatMap,顾名思义,是mapflat操作的组合。这意味着您首先对元素应用函数,然后将其平铺。而Stream.map仅将函数应用于流,而不会将流平铺。

要理解什么是“平铺”流,考虑一个结构如[ [1,2,3],[4,5,6],[7,8,9] ],它有“两层”结构。将其平铺意味着将其转换为“一层”结构:[ 1,2,3,4,5,6,7,8,9 ]


10
哈哈,公平地说,我仍然很惊讶看到这个问题有多少的流量。另一个有趣的观察是,我写下这个答案已经将近5年了,而被采纳的答案得到的赞数与我的回答得到的赞数大致为2比1。这个趋势出奇地稳定。 - Dici
9
这怎么不是被接受的答案呢?感谢您直截了当地讲解并提供一个非常简单的例子。 - 1mike12
6
我认为这个答案比被采纳的答案更加简洁,并且逻辑上更好地解释了问题。 - Morteza
3
这个答案简短实用。被采纳的(Stuart 的)答案解释了为什么需要 flatMap 这种方法。每个答案都适合回答社区可能寻求的不同类型问题。 - aff
简洁明了的解释 - Jayavignesh Vicky
显示剩余2条评论

319

我想给出两个例子来提供一个更实际的观点:
第一个例子使用了 map

@Test
public void convertStringToUpperCaseStreams() {
    List<String> collected = Stream.of("a", "b", "hello") // Stream of String 
            .map(String::toUpperCase) // Returns a stream consisting of the results of applying the given function to the elements of this stream.
            .collect(Collectors.toList());
    assertEquals(asList("A", "B", "HELLO"), collected);
}

第一个示例没有什么特别的,应用了一个 Function 来返回大写的 String

第二个示例使用了 flatMap

@Test
public void testflatMap() throws Exception {
    List<Integer> together = Stream.of(asList(1, 2), asList(3, 4)) // Stream of List<Integer>
            .flatMap(List::stream)
            .map(integer -> integer + 1)
            .collect(Collectors.toList());
    assertEquals(asList(2, 3, 4, 5), together);
}

在第二个例子中,传递了一个ListStream它不是IntegerStream


如果需要使用转换函数(通过map),则首先必须将Stream展平为其他内容(IntegerStream)。
如果移除flatMap,则会返回以下错误:运算符+对于参数类型(List、int)未定义。
不可能对ListInteger应用+1操作!


@PrashanthDebbadwar 我认为你最终将得到一个 Stream<Integer> 的流而不是一个 Integer 的流。 - payne

207

请完整阅读文章以获得清晰的理解

map和flatMap:

如果我们想要返回一个列表中每个单词的长度,我们可以像下面这样做..

以下是简短版

当我们收集两个列表时,如下所示

没有使用 flatMap => [1,2],[1,1] => [[1,2],[1,1]] 这里将两个列表放入一个列表中,因此输出将是包含列表的列表

使用了 flatMap => [1,2],[1,1] => [1,2,1,1] 这里将两个列表平铺并且只将值放入列表,因此输出将仅包含元素

基本上它将所有对象合并成一个

## 下面给出详细版本:

例如:
考虑一个列表[“STACK”, ”OOOVVVER”],我们试图返回一个列表[“STACKOVER”](从该列表中仅返回唯一字母)。最初,我们会像下面这样做,从[“STACK”, ”OOOVVVER”]返回一个列表[“STACKOVER”]

public class WordMap {
  public static void main(String[] args) {
    List<String> lst = Arrays.asList("STACK","OOOVER");
    lst.stream().map(w->w.split("")).distinct().collect(Collectors.toList());
  }
}

问题在于,传递给map方法的Lambda返回一个String数组表示每个单词,因此由map方法返回的流实际上是Stream类型,但我们需要Stream来表示字符流,下面的图示说明了这个问题。

图A:

enter image description here

你可能会认为,我们可以使用flatmap解决这个问题,好吧,让我们看看如何通过使用mapArrays.stream来解决这个问题。首先,你需要一个字符流而不是一个数组流。有一个叫做Arrays.stream()的方法可以取一个数组并产生一个流,例如:

String[] arrayOfWords = {"STACK", "OOOVVVER"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
streamOfWords.map(s->s.split("")) //Converting word in to array of letters
    .map(Arrays::stream).distinct() //Make array in to separate stream
    .collect(Collectors.toList());

上述方法仍然不起作用,因为我们最终得到了一个流列表(更精确地说是Stream>),相反,我们必须先将每个单词转换为单独字母的数组,然后将每个数组转换为单独的流。

通过使用flatMap,我们应该能够解决这个问题,如下所示:

String[] arrayOfWords = {"STACK", "OOOVVVER"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
streamOfWords.map(s->s.split("")) //Converting word in to array of letters
    .flatMap(Arrays::stream).distinct() //flattens each generated stream in to a single stream
    .collect(Collectors.toList());

flatMap执行的是将每个数组的元素映射成一个流,而不是像map(Arrays::stream)那样映射成一个包含流的流。所有生成的单独流都会合并到一个单一的流中。图B说明了使用flatMap方法的效果。与图A中map的效果进行比较。

图B enter image description here

flatMap方法允许您将流的每个值替换为另一个流,然后将所有生成的流合并到单个流中。


4
清晰易懂的图示解释。 - Hitesh
1
@TechDog,非常感谢您的示例。我之前一直卡在如何将行拆分为单词并流式处理它们上。 - likejudo
2
非常感谢您基于实际示例和插图进行的解释。这对于理解非常有帮助。 - invzbl3

157
一句话简述:flatMap可以将一个`Collection>`扁平化为一个`Collection`,同理也能将一个`Optional>`扁平化为一个`Optional`。

enter image description here

如您所见,仅使用 map()

  • 中间类型是 Stream<List<Item>>
  • 返回类型是 List<List<Item>>

而使用 flatMap()

  • 中间类型是 Stream<Item>
  • 返回类型是 List<Item>

这是下面使用的代码的测试结果

-------- Without flatMap() -------------------------------
     collect() returns: [[Laptop, Phone], [Mouse, Keyboard]]

-------- With flatMap() ----------------------------------
     collect() returns: [Laptop, Phone, Mouse, Keyboard]

代码使用:

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class Parcel {
  String name;
  List<String> items;

  public Parcel(String name, String... items) {
    this.name = name;
    this.items = Arrays.asList(items);
  }

  public List<String> getItems() {
    return items;
  }

  public static void main(String[] args) {
    Parcel amazon = new Parcel("amazon", "Laptop", "Phone");
    Parcel ebay = new Parcel("ebay", "Mouse", "Keyboard");
    List<Parcel> parcels = Arrays.asList(amazon, ebay);

    System.out.println("-------- Without flatMap() ---------------------------");
    List<List<String>> mapReturn = parcels.stream()
      .map(Parcel::getItems)
      .collect(Collectors.toList());
    System.out.println("\t collect() returns: " + mapReturn);

    System.out.println("\n-------- With flatMap() ------------------------------");
    List<String> flatMapReturn = parcels.stream()
      .map(Parcel::getItems)
      .flatMap(Collection::stream)
      .collect(Collectors.toList());
    System.out.println("\t collect() returns: " + flatMapReturn);
  }
}

14
非常清晰的例子...使用你的例子理解这个概念不需要花费几秒钟以上的时间... - TechDog
谢谢你提到将 Optional<Optional<T>> 转换为 Optional<T>。否则,我就会回答了! - manikanta nvsr

59

.map 是用于进行 A -> B 映射的

Stream.of("dog", "cat")              // stream of 2 Strings
    .map(s -> s.length())            // stream of 2 Integers: [3, 3]

它将任何项目A转换为任何项目BJavadoc


.flatMap用于将A -> Stream< B>连接起来

Stream.of("dog", "cat")             // stream of 2 Strings
    .flatMapToInt(s -> s.chars())   // stream of 6 ints:      [d, o, g, c, a, t]

it --1 将任何项目 A 转换为 Stream<B>,然后 --2 将所有的流连接成一个(扁平化)流。Javadoc


注意 1:尽管后面的示例将其转换为基本类型的流(IntStream),而不是对象的流(Stream),但它仍说明了 .flatMap 的思想。

注意 2:尽管名称为 String.chars() 方法返回 int 值。因此,实际的集合将是:[100, 111, 103, 99, 97, 116],其中 100 是字符 'd' 的代码,111 是字符 'o' 的代码等。同样,为了说明目的,它被表示为 [d,o,g,c,a,t]。


只需将其替换为 toCharArray 并缩短您的帖子。 - Simon Logic

54

您传递给stream.map的函数必须返回一个对象。这意味着输入流中的每个对象都会在输出流中产生一个对象。

您传递给stream.flatMap的函数为每个对象返回一个流。这意味着该函数可以为每个输入对象返回任意数量的对象(包括没有)。然后将生成的流连接到一个输出流中。


为什么您想要“为每个输入对象(包括无)返回任意数量的对象”? - Derek Mahar
4
@DerekMahar 这个有很多应用场景。例如,假设您的组织中有一系列“部门”。每个部门都有0到n个“员工”。你需要一个所有员工的流。那么你该怎么做呢?你可以编写一个flatMap方法,它接受一个部门并返回其员工流。 - Philipp
菲利普,你的例子是否阐明了使用 flatMap 的主要原因?我怀疑这可能只是偶然的,没有阐明 flatMap 存在的关键用例或原因。(以下继续...) - Derek Mahar
阅读完https://dzone.com/articles/understanding-flatmap后,我认为`flatMap`背后的主要动机是为了容纳在使用`map`时存在的错误。当原始集合中的一个或多个项目无法映射到输出项目时,您如何处理这种情况?通过为每个输入对象引入一个中间集(例如`Optional`或`Stream`),`flatMap`允许您从最终集合中排除“无效”的输入对象(或者按照https://dev59.com/L18d5IYBdhLWcg3wkS0V#52248643的精神所说的“坏苹果”)。 - Derek Mahar
1
@DerekMahar 是的,每个输入对象可能会或可能不会返回输出对象的情况是 flat-map 的另一个好用例。 - Philipp
通常情况下,你会使用stream.filter方法来实现这个功能。 - undefined

38

对于一个 Map,我们有一个元素列表和一个(函数,操作)f,因此:

[a,b,c] f(x) => [f(a),f(b),f(c)]

对于平面地图,我们有一个元素列表,并且我们有一个 (函数,操作) f,我们希望结果被压扁:

[[a,b],[c,d,e]] f(x) =>[f(a),f(b),f(c),f(d),f(e)]

32
我觉得这里大多数答案都过于复杂化了简单的问题。如果你已经理解了 map 的工作原理,那么应该很容易理解。
在使用 map() 时,有些情况下我们可能会得到不需要的嵌套结构,flatMap() 方法旨在通过避免包装来克服这一问题。

例子:

1

List<List<Integer>> result = Stream.of(Arrays.asList(1), Arrays.asList(2, 3))
  .collect(Collectors.toList());

我们可以通过使用flatMap来避免嵌套列表:

List<Integer> result = Stream.of(Arrays.asList(1), Arrays.asList(2, 3))
  .flatMap(i -> i.stream())
  .collect(Collectors.toList());

2

Optional<Optional<String>> result = Optional.of(42)
      .map(id -> findById(id));

Optional<String> result = Optional.of(42)
      .flatMap(id -> findById(id));

其中:

private Optional<String> findById(Integer id)

抱歉,第1点中的第2个代码片段无法编译。正确的方式应该是:Stream.of(Arrays.asList(1), Arrays.asList(2, 3)) .flatMap(List::stream) .collect(Collectors.toList()); - arthur
@Arthur 我认为我在这里使用了Vavr的Stream和List - 但我同意这可能会有点令人困惑 - 我会将其改为标准的Java。 - Grzegorz Piwowarek
@GrzegorzPiwowarek 这个简单的解释怎么样?(https://dev59.com/L18d5IYBdhLWcg3wkS0V#52248643) ? - Eugene

32

map() 和 flatMap()

  1. map()

它只需一个函数 <T, R>,其中 T 是元素,R 是使用 T 构建的返回元素。最后,我们将得到一个类型为 R 的流。一个简单的示例可以是:

Stream
  .of(1,2,3,4,5)
  .map(myInt -> "preFix_"+myInt)
  .forEach(System.out::println);

这段代码简单地使用Type Integer的1到5个元素,用每个元素构建一个新元素,类型为String,值为"prefix_"+integer_value,并将其打印出来。

  1. flatMap()

了解flatMap()非常有用,它接受一个函数F<T, R>,其中:

  • T是一种类型,可以从中构建/使用Stream。它可以是List(T.stream()),数组(Arrays.stream(someArray))等任何可以用Stream构建/形成的东西。在下面的示例中,每个开发人员都有很多语言,因此dev.Languages是一个List,并将使用lambda参数。

  • R是将使用T构建的结果流。知道我们有许多T的实例,我们自然会从R中拥有许多流。所有这些来自Type R的流现在将合并为一个单一的“扁平”流,来自Type R。

示例

Bachiri Taoufiq的示例[在此处查看其答案] 1 简单易懂。只是为了清晰起见,假设我们有一个开发团队:

dev_team = {dev_1,dev_2,dev_3}

每个开发人员都要掌握多种语言:

dev_1 = {lang_a,lang_b,lang_c},
dev_2 = {lang_d},
dev_3 = {lang_e,lang_f}

应用 Stream.map() 在 dev_team 上以获取每个开发人员的语言:

dev_team.map(dev -> dev.getLanguages())

将会为您提供这个结构:

{ 
  {lang_a,lang_b,lang_c},
  {lang_d},
  {lang_e,lang_f}
}

这段代码基本上是一个 List<List<Languages>> /Object[Languages[]] 的结构,看起来不是很美观,也不像Java8风格!!

使用 Stream.flatMap() 你可以将这个结构“展平”,并将其转换成 {lang_a, lang_b, lang_c, lang_d, lang_e, lang_f},这基本上可以用作 List<Languages>/Language[]/etc...

因此,最终你的代码应该像这样更有意义:

dev_team
   .stream()    /* {dev_1,dev_2,dev_3} */
   .map(dev -> dev.getLanguages()) /* {{lang_a,...,lang_c},{lang_d}{lang_e,lang_f}}} */
   .flatMap(languages ->  languages.stream()) /* {lang_a,...,lang_d, lang_e, lang_f} */
   .doWhateverWithYourNewStreamHere();

或者简单地说:
dev_team
       .stream()    /* {dev_1,dev_2,dev_3} */
       .flatMap(dev -> dev.getLanguages().stream()) /* {lang_a,...,lang_d, lang_e, lang_f} */
       .doWhateverWithYourNewStreamHere();

何时使用map()和flatMap():

  • 当您的流中每个类型为T的元素应被映射/转换为类型为R的单个元素时,请使用map()。结果是一种类型为(1个起始元素 -> 1个结束元素)的映射,并返回新的类型为R的元素流。

  • 当您的流中每个类型为T的元素应被映射/转换为类型为R的集合中的元素时,请使用flatMap()。结果是一种类型为(1个起始元素 -> n个结束元素)的映射。然后这些集合会被合并(或展平)成一个新的类型为R的元素流。例如,这对于表示嵌套循环很有用。

Java 8之前:

List<Foo> myFoos = new ArrayList<Foo>();
    for(Foo foo: myFoos){
        for(Bar bar:  foo.getMyBars()){
            System.out.println(bar.getMyName());
        }
    }

Java 8之后

myFoos
    .stream()
    .flatMap(foo -> foo.getMyBars().stream())
    .forEach(bar -> System.out.println(bar.getMyName()));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接