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个回答

25

Oracle在Optional文章中强调了map和flatmap之间的差异:

String version = computer.map(Computer::getSoundcard)
                  .map(Soundcard::getUSB)
                  .map(USB::getVersion)
                  .orElse("UNKNOWN");

很不幸,这段代码无法编译。为什么?变量computer的类型是Optional,因此调用map方法是完全正确的。但是,getSoundcard()返回的是Optional类型的对象。这意味着map操作的结果是一个Optional>类型的对象。因此,对getUSB()的调用是无效的,因为最外层的Optional包含另一个Optional作为其值,而后者当然不支持getUSB()方法。 使用流时,flatMap方法接受一个函数作为参数,该函数返回另一个流。该函数应用于流的每个元素,这将导致流中有多个流。但是,flatMap的效果是用该流的内容替换每个生成的流。换句话说,由函数生成的所有单独流都被合并或“展平”成一个单一流。我们想要的是类似的东西,但是我们想要将一个二级Optional“展平”为一个。 Optional还支持flatMap方法。它的目的是在Optional的值上应用转换函数(就像map操作一样),然后将生成的二级Optional“展平”为单个Optional。 因此,为了使我们的代码正确,我们需要使用flatMap重写它,如下所示:
String version = computer.flatMap(Computer::getSoundcard)
                   .flatMap(Soundcard::getUSB)
                   .map(USB::getVersion)
                   .orElse("UNKNOWN");

第一个flatMap确保返回一个Optional而不是Optional>,第二个flatMap也实现了相同的目的返回一个Optional。注意,第三次调用只需要使用map(),因为getVersion()返回一个String而不是Optional对象。

http://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html


1
问题是关于 Stream.map 和 Stream.flatMap,而不是 Optional.map 和 Optional.flatMap。 - djames
4
非常感谢,这帮助我更好地理解了我在使用可选项和flatmap时遇到的问题! - Loïc
2
@djames,这是一个完全有效的答案,请从“使用流时,flatMap方法接受一个函数作为参数…”一段开始阅读 :) - skwisgaar
flatMap() 版本如果 soundCard 为 null,也会抛出 NullPointerException。那么 Optional 的承诺在哪里呢? - Ekaterina
@Ekaterina,这是处理Optional指针的方法,如果getter返回Optional,那么现在就不会抛出NullPointerException了。 - Mike

19

我不太确定是否应该回答这个问题,但每当我遇到不理解的人时,我都会使用同样的例子。

想象一下你有一个苹果。一个map就像是将这个苹果变成例如apple-juice或者是一个一对一映射。

取同一个苹果,只取出里面的种子,这就是flatMap的作用,或者是一个一对多,一个苹果作为输入,多个种子作为输出。


对于 flatMap 的情况,您是先将每个苹果的种子收集到单独的袋子中(一个苹果对应一个袋子),然后再将所有的袋子倒入单个袋子中吗? - Derek Mahar
@DerekMahar 在 Java 10 之前,它曾经是将多个元素压缩成一个袋子的方式,这意味着 flatmap 并不是真正的惰性求值,但自从 Java 10 以后,它就变得惰性了。 - Eugene
@Eugene,请再解释一下你试图解释的“懒惰概念”,因为我还不太清楚。我理解了derkerMahar在评论中所解释的内容,这是在Java10之前发生的事情吗? - JAVA
@Eugene - flatMap会一次取一个苹果,提取其种子并将其扔进袋子/集合/数组中,直到所有苹果的种子都被提取完为止。对吗?这就是流的工作方式吗? - MasterJoe
1
@Eugene 我不明白。种子和果汁是不同的。据我所知,flatMap 应该将多个流缩减为单个流。我想知道你的比喻是否不正确。 - likejudo
显示剩余4条评论

14

Map方法:该方法接受一个函数作为参数,并返回一个新的流,其中包含将传递的函数应用于流所有元素生成的结果。

假设我有一个整数值列表(1、2、3、4、5),以及一个函数接口,其逻辑是将传递的整数平方(e->e * e)。

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> newList = intList.stream().map( e -> e * e ).collect(Collectors.toList());

System.out.println(newList);

输出:-

[1, 4, 9, 16, 25]

正如您所看到的,输出是一个新的流,其值是输入流的平方值。
[1, 2, 3, 4, 5] -> apply e -> e * e -> [ 1*1, 2*2, 3*3, 4*4, 5*5 ] -> [1, 4, 9, 16, 25 ]

http://codedestine.com/java-8-stream-map-method/

FlatMap :- 该方法接受一个函数作为参数,这个函数以T类型的参数作为输入参数,并返回一个R类型的流作为返回值。当将此函数应用于此流的每个元素时,它会生成一系列新值的流。由每个元素生成的这些新流的所有元素都将被复制到一个新流中,这个新流将是此方法的返回值。

假设我有一个学生对象的列表,每个学生可以选择多个科目。

List<Student> studentList = new ArrayList<Student>();

  studentList.add(new Student("Robert","5st grade", Arrays.asList(new String[]{"history","math","geography"})));
  studentList.add(new Student("Martin","8st grade", Arrays.asList(new String[]{"economics","biology"})));
  studentList.add(new Student("Robert","9st grade", Arrays.asList(new String[]{"science","math"})));

  Set<Student> courses = studentList.stream().flatMap( e -> e.getCourse().stream()).collect(Collectors.toSet());

  System.out.println(courses);

输出:-

[economics, biology, geography, science, history, math]

如你所见,一个输出是一个新的流,其值是由输入流的每个元素返回的流中所有元素的集合。

[ S1 , S2 , S3 ] -> [ {"历史","数学","地理"}, {"经济学","生物学"}, {"科学","数学"} ] -> 取唯一的科目 -> [经济学, 生物学, 地理, 科学, 历史, 数学]

http://codedestine.com/java-8-stream-flatmap-method/


提供代码而不仅仅是文档链接可能会有所不同。 - Charles-Antoine Fournel
你的输入有误吧?应该在这里使用一个字符串集合而不是学生集合。代码如下:Set<String> courses = studentList.stream().flatMap( e -> e.getCourse().stream()).collect(Collectors.toSet()); - BabyishTank

11

通过阅读所有消息,简单的理解方式是:

  • 如果您有一个“平坦”的元素列表:[0, 1, 2, 3, 4, 5],请使用 map
  • 如果您有一个元素的列表的列表:[[1, 3, 5], [2, 4, 6]],请使用 flatMap。这意味着,在对每个元素应用 map 操作之前,需要将您的列表扁平化

8
如果您将map()视为迭代(单层循环),则flatmap()是双层迭代(嵌套循环)。输入每个迭代的元素foo,执行foo.getBarList()并再次在barList中迭代。
map():获取流,对每个元素进行操作,收集每个过程的单一结果,输出另一个流。 "do something function"的定义是隐含的。如果任何元素的处理结果为null,则使用null来组成最终流。因此,结果流中的元素数将等于输入流的数量。 flatmap():获取元素/流的流和一个函数(明确定义),将该函数应用于每个流的每个元素,并收集所有中间结果流以形成更大的流(“扁平化”)。如果任何元素的处理结果为null,则空流将提供给“扁平化”的最终步骤。如果输入是几个流,则结果流中的元素数是所有参与元素的总和。

5

简单回答。

map操作可以生成一个StreamStream。例如:Stream<Stream<Integer>>

flatMap操作将只生成某种类型的Stream。例如:Stream<Integer>


3

这些Stream API方法的定义:

map(Function mapper)是一种中间操作,它返回一个流,该流由将给定函数应用于此流的元素的结果组成。

flatMap(Function mapper)是一种中间操作,它返回一个流,该流由将此流的每个元素替换为通过将提供的映射函数应用于每个元素而生成的映射流的内容组成。简而言之,它用于将Stream of Stream转换为值列表。

看一个例子:

List<String> myList = Stream.of("Apple", "Orange")
       .map(String::toUpperCase)
       .collect(Collectors.toList());
System.out.println(myList);

它会打印:[APPLE, ORANGE]

我们可以将map()更改为flatMap()吗?

List<String> myListFlat = Stream.of("Apple", "Orange")
       .flatMap(String::toUpperCase)
       .collect(Collectors.toList());
System.out.println(myListFlat);

不好意思,我们现在能看到一个错误:

java: incompatible types: cannot infer type-variable(s) R
    (argument mismatch; bad return type in method reference
      java.lang.String cannot be converted to java.util.stream.Stream<? extends R>)

为什么会这样呢?看一下方法签名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

代码无法运行的原因是flatMap()方法期望返回一个流的函数。在第二个示例中,我将一个方法引用传递给String.toUpperCase(),它返回一个字符串。因此,我试图在字符流而不是字符串流上应用flatMap()
好的,现在我们可以知道flatMap()的用途了。 让我们构建一个列表的列表:
List<List<String>> list = Arrays.asList(
            Arrays.asList("Apple"),
            Arrays.asList("Orange"));
System.out.println(list);

并且它会打印出: [[Apple], [Orange]]。如果我们想要将它展平,让我们使用 flatMap():

List<String> flattedList = list
                .stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toList());

System.out.println(flattedList);

现在它会打印:[苹果,橙子]

Sources: https://www.geeksforgeeks.org/stream-flatmap-java-examples/

https://www.javatpoint.com/flatmap-method-in-java-8

https://www.baeldung.com/java-difference-map-and-flatmap


1
flatMap() 还利用了流的部分惰性求值。它将读取第一个流,并且只有在需要时才会转到下一个流。该行为在此处详细说明:flatMap是否保证是惰性的?

1

流操作flatMapmap接受一个函数作为输入。

flatMap期望函数返回每个流的新流,并返回由函数返回的每个元素的流组合而成的流。换句话说,使用flatMap,对于源中的每个元素,函数将创建多个元素。http://www.zoftino.com/java-stream-examples#flatmap-operation

map期望函数返回转换后的值,并返回包含转换元素的新流。换句话说,使用map,对于源中的每个元素,函数将创建一个转换元素。http://www.zoftino.com/java-stream-examples#map-operation


1
这对初学者来说很困惑。基本区别在于map为列表中的每个条目发出一个项,而flatMap基本上是一个map+flatten操作。更明确地说,当您需要多个值时,请使用flatMap,例如当您期望循环返回数组时,在这种情况下,flatMap将非常有用。

我已经写了一篇关于此的博客,您可以在这里查看。


这个简单的解释怎么样? - Eugene

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