Java 8中的Streams FlatMap方法示例

91

我一直在关注即将到来的Java更新,即:Java 8或JDK 8。是的,我很着急,因为有很多新东西,但是,有一些简单的代码我不理解:

final Stream<Integer>stream = Stream.of(1,2,3,4,5,6,7,8,9,10);
stream.flatMap();

javadoc 是 Java 代码文档,

public <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

将此流的每个元素替换为应用提供的映射函数后生成的映射流的内容,返回由结果组成的流。每个映射流在其内容被放入此流后都会关闭。如果映射流为 null,则使用空流代替。 这是一项中间操作。

我希望有人能创建一些关于 flatMap 的简单实际示例,并展示如何在之前的 Java 版本 Java[6,7] 中编码以及如何使用 Java 8 编写相同的程序。


2
互联网上有大约一百万个使用flatMap的例子(至少是针对Scala的,而且它们基本上都是相同的:)你试过搜索吗?这里有一个开始的链接:http://www.brunton-spall.co.uk/post/2011/12/02/map-map-and-flatmap-in-scala/ - Peter Svensson
3
我不懂Scala,从未使用过它。 - chiperortiz
我的意思是flatMap是一个通用的概念,现在已经存在于Java和Scala中。 - Peter Svensson
好的,我会多了解一下,谢谢。 - chiperortiz
11
在Java中,flatMap与流(streams)使用了相同的理念,但它们的表现形式却有很大不同。请不要指向Scala! - orbfish
请查看:https://dev59.com/L18d5IYBdhLWcg3wkS0V#37080387 - TechDog
7个回答

164

对于已经扁平的,比如你在问题中展示的Stream<Integer>,使用flatMap是没有意义的。

不过,如果你有一个Stream<List<Integer>>,那么使用flatMap就有意义了,你可以这样做:

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

Stream<Integer> integerStream = integerListStream .flatMap(Collection::stream);
integerStream.forEach(System.out::println);

将会打印:

1
2
3
4
5

在Java 8之前,你只需要使用循环即可实现此操作:

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

List<Integer> flattened = new ArrayList<>();

for (List<Integer> integerList : integerLists) {
    flattened.addAll(integerList);
}

for (Integer i : flattened) {
    System.out.println(i);
}

我无法理解flatMap中的Collection::stream。如果flatMap已经返回了一个流,为什么我需要在flatMap内部对元素进行stream()操作呢?在我的想象中,执行integerListStream.flatMap(Collection::stream)会返回Stream<Stream<Integer>>,因为我对每个Integer都调用了stream()方法,然后将其返回为一个流... 我的头痛了。 - Vinicius Milani
1
传递给 flatMapFunction 负责创建 Stream,因此我给出的示例实际上是 integerListStream.flatMap(ints -> ints.stream()) 的简写,其中 ints 是来自 integerLists 的整数列表。 - Nick Holt

119

虚构的例子

想象一下,您想创建以下序列:1、2、2、3、3、3、4、4、4、4等(换句话说:1x1、2x2、3x3等)。

使用flatMap,它可能如下所示:

IntStream sequence = IntStream.rangeClosed(1, 4)
                          .flatMap(i -> IntStream.iterate(i, identity()).limit(i));
sequence.forEach(System.out::println);

其中:

  • IntStream.rangeClosed(1, 4) 创建一个从1到4的int类型流(包括1和4)
  • IntStream.iterate(i, identity()).limit(i) 创建长度为i的int类型流,其中每个元素都是i。例如对于i=4,它创建的流是:4, 4, 4, 4
  • flatMap "扁平化" 流并将其"连接"到原始流中

在Java 8之前,您需要两个嵌套循环:

List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 4; i++) {
    for (int j = 0; j < i; j++) {
        list.add(i);
    }
}

现实世界的例子

假设我有一个List<TimeSeries>,其中每个TimeSeries基本上都是一个Map<LocalDate,Double>。 我想要获取所有至少有一个时间序列具有值的日期列表。通过使用flatMap来解决:

list.stream().parallel()
    .flatMap(ts -> ts.dates().stream()) // for each TS, stream dates and flatmap
    .distinct()                         // remove duplicates
    .sorted()                           // sort ascending
    .collect(toList());

不仅易读,而且如果您突然需要处理100k个元素,只需添加parallel()即可提高性能,而无需编写任何并发代码。


15
两个例子都比被接受的答案好得多。 - Sebastian Graf
编译器抱怨identity()未定义。 - Nirmal
2
@user3320018,你需要静态导入 Function.identity - assylias
@assylias 我尝试了 import java.util.function.Function 但是没有起作用,我是Java 8的新手,这可能与Java 8有关,也可能不是,但您能否告诉我如何确切地消除该错误。 - Nirmal
4
导入静态的java.util.function.Function.identity函数。 - assylias

22

从短语列表中提取按升序排列的唯一单词:

List<String> phrases = Arrays.asList(
        "sporadic perjury",
        "confounded skimming",
        "incumbent jailer",
        "confounded jailer");

List<String> uniqueWords = phrases
        .stream()
        .flatMap(phrase -> Stream.of(phrase.split("\\s+")))
        .distinct()
        .sorted()
        .collect(Collectors.toList());
System.out.println("Unique words: " + uniqueWords);

...和输出:

Unique words: [confounded, incumbent, jailer, perjury, skimming, sporadic]

11

我是唯一一个觉得展开列表很无聊的人吗?;-)

我们尝试使用对象。顺便说一下,这是一个真实世界的例子。

假设你有一个表示重复任务的对象。其中,关于重要任务字段:提醒将从start开始响起,并每隔repeatPeriodrepeatUnit(例如5小时)重复一次。总共会有repeatCount个提醒(包括开始提醒)。

目标:实现任务副本列表,为每个任务提醒调用生成一个副本。

List<Task> tasks =
            Arrays.asList(
                    new Task(
                            false,//completed sign
                            "My important task",//task name (text)
                            LocalDateTime.now().plus(2, ChronoUnit.DAYS),//first reminder(start)
                            true,//is task repetitive?
                            1,//reminder interval
                            ChronoUnit.DAYS,//interval unit
                            5//total number of reminders
                    )
            );

tasks.stream().flatMap(
        x -> LongStream.iterate(
                x.getStart().toEpochSecond(ZoneOffset.UTC),
                p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds())
        ).limit(x.getRepeatCount()).boxed()
        .map( y -> new Task(x,LocalDateTime.ofEpochSecond(y,0,ZoneOffset.UTC)))
).forEach(System.out::println);

输出:

Task{completed=false, text='My important task', start=2014-10-01T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-02T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-03T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-04T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}
Task{completed=false, text='My important task', start=2014-10-05T21:35:24, repeat=false, repeatCount=0, repeatPeriod=0, repeatUnit=null}

P.S.: 如果有人能提供更简单的解决方案,我将不胜感激。毕竟,我不是专业人士。

更新: @RBz 要求详细解释,因此在这里进行解释。 基本上,flatMap 将流中所有元素放入另一个流中并输出。这里有很多流 :)。因此,对于初始流中的每个任务,lambda 表达式 x -> LongStream.iterate... 创建表示任务开始时刻的 long 值流。该流被限制为 x.getRepeatCount() 个实例。其值从 x.getStart().toEpochSecond(ZoneOffset.UTC) 开始,每个下一个值使用 lambda p -> (p + x.getRepeatPeriod()*x.getRepeatUnit().getDuration().getSeconds() 计算。 boxed() 返回具有每个 long 值作为 Long 包装器实例的流。然后,该流中的每个 Long 都映射到新的 Task 实例,该实例不再重复,并且包含确切的执行时间。此示例仅包含输入列表中的一个任务。但是想象一下您有一千个。那么您将拥有 1000 个 Task 对象流的流。在这里,flatMap 所做的就是将所有流中的所有任务放入同一输出流中。就我所知,就是这样。感谢您的问题!


8
我是唯一一个觉得看列表很无聊的人吗? - whitfin
3
我很难理解这个例子。 :( - RBz
@RBz 流操作有时不易理解,特别是涉及多个操作时。但这只是一个实践问题。最好的方法是从示例中谷歌每个不清楚的单词,并尝试自己使用它们。事实上,通常的命令式样本会更容易理解(有时更快)。所以请考虑是否真的需要使用流。 - Aleksandr Kravets
谢谢你的回复。然而,我对流的概念还算熟悉。我在这里遇到的问题是针对具体示例的。我之前对时间API并不是很了解,但是即使仔细阅读也无法帮助我理解这里发生了什么。也许我太天真了,但如果你能再多解释一下你的答案,那将非常有帮助。这将真正帮助我理解你的示例。我知道,我只是因为好奇才会这样纠结! :) - RBz
惊人的例子...一开始有点难理解,但是一旦在我的IDE中运行它...就是如此强大的替代品!! 非常感谢! - Cristiano

2

一个非常简单的例子:将完整姓名列表拆分为名称列表,无论名字还是姓。

 List<String> fullNames = Arrays.asList("Barry Allen", "Bruce Wayne", "Clark Kent");

 fullNames.stream()
            .flatMap(fullName -> Pattern.compile(" ").splitAsStream(fullName))
            .forEach(System.out::println);

这将打印出:
Barry
Allen
Bruce
Wayne
Clark
Kent

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

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


1
给定这个:
  public class SalesTerritory
    {
        private String territoryName;
        private Set<String> geographicExtents;

        public SalesTerritory( String territoryName, Set<String> zipCodes )
        {
            this.territoryName = territoryName;
            this.geographicExtents = zipCodes;
        }

        public String getTerritoryName()
        {
            return territoryName;
        }

        public void setTerritoryName( String territoryName )
        {
            this.territoryName = territoryName;
        }

        public Set<String> getGeographicExtents()
        {
            return geographicExtents != null ? Collections.unmodifiableSet( geographicExtents ) : Collections.emptySet();
        }

        public void setGeographicExtents( Set<String> geographicExtents )
        {
            this.geographicExtents = new HashSet<>( geographicExtents );
        }

        @Override
        public int hashCode()
        {
            int hash = 7;
            hash = 53 * hash + Objects.hashCode( this.territoryName );
            return hash;
        }

        @Override
        public boolean equals( Object obj )
        {
            if ( this == obj ) {
                return true;
            }
            if ( obj == null ) {
                return false;
            }
            if ( getClass() != obj.getClass() ) {
                return false;
            }
            final SalesTerritory other = (SalesTerritory) obj;
            if ( !Objects.equals( this.territoryName, other.territoryName ) ) {
                return false;
            }
            return true;
        }

        @Override
        public String toString()
        {
            return "SalesTerritory{" + "territoryName=" + territoryName + ", geographicExtents=" + geographicExtents + '}';
        }

    }

和这个:

public class SalesTerritories
{
    private static final Set<SalesTerritory> territories
        = new HashSet<>(
            Arrays.asList(
                new SalesTerritory[]{
                    new SalesTerritory( "North-East, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Maine", "New Hampshire", "Vermont",
                                                                                    "Rhode Island", "Massachusetts", "Connecticut",
                                                                                    "New York", "New Jersey", "Delaware", "Maryland",
                                                                                    "Eastern Pennsylvania", "District of Columbia" } ) ) ),
                    new SalesTerritory( "Appalachia, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "West-Virgina", "Kentucky",
                                                                                    "Western Pennsylvania" } ) ) ),
                    new SalesTerritory( "South-East, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Virginia", "North Carolina", "South Carolina",
                                                                                    "Georgia", "Florida", "Alabama", "Tennessee",
                                                                                    "Mississippi", "Arkansas", "Louisiana" } ) ) ),
                    new SalesTerritory( "Mid-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Ohio", "Michigan", "Wisconsin", "Minnesota",
                                                                                    "Iowa", "Missouri", "Illinois", "Indiana" } ) ) ),
                    new SalesTerritory( "Great Plains, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Oklahoma", "Kansas", "Nebraska",
                                                                                    "South Dakota", "North Dakota",
                                                                                    "Eastern Montana",
                                                                                    "Wyoming", "Colorada" } ) ) ),
                    new SalesTerritory( "Rocky Mountain, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Western Montana", "Idaho", "Utah", "Nevada" } ) ) ),
                    new SalesTerritory( "South-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Arizona", "New Mexico", "Texas" } ) ) ),
                    new SalesTerritory( "Pacific North-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "Washington", "Oregon", "Alaska" } ) ) ),
                    new SalesTerritory( "Pacific South-West, USA",
                                        new HashSet<>( Arrays.asList( new String[]{ "California", "Hawaii" } ) ) )
                }
            )
        );

    public static Set<SalesTerritory> getAllTerritories()
    {
        return Collections.unmodifiableSet( territories );
    }

    private SalesTerritories()
    {
    }

}

我们可以这样做:

然后我们可以执行以下操作:

System.out.println();
System.out
    .println( "We can use 'flatMap' in combination with the 'AbstractMap.SimpleEntry' class to flatten a hierarchical data-structure to a set of Key/Value pairs..." );
SalesTerritories.getAllTerritories()
    .stream()
    .flatMap( t -> t.getGeographicExtents()
        .stream()
        .map( ge -> new SimpleEntry<>( t.getTerritoryName(), ge ) )
    )
    .map( e -> String.format( "%-30s : %s",
                              e.getKey(),
                              e.getValue() ) )
    .forEach( System.out::println );

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