Java 8 Stream 教程 (一)

作者:Benjamin

译者:java达人

来源:http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/(点击阅读原文前往)

这一示例驱动的教程对Java 8stream进行了深入的阐述。当我第一次读到streamAPI时,我对它的名称感到困惑,因为它听起来类似于Java I/ O的InputStream和OutputStream。但是Java 8 Stream是完全不同的东西。Stream是Monads,因此在将函数编程引入Java方面起了很大作用:

在函数式编程中,monad是一个表示计算(步骤序列)的结构。一个带有monad结构的类型或该类型的嵌套函数定义了其链式操作的意义。

本指南教你如何使用Java 8 Stream,以及如何使用不同种类的可用的stream操作。您将了解处理顺序以及stream操作的排序如何影响运行时性能。更强大的stream操作 reduce, collectand,flatMap会详细讨论。本教程结尾会深入研究并行stream。

如果您还不熟悉Java 8 lambda表达式、函数接口和方法引用,那么您可能希望在开始学习本教程之前先阅读我的Java 8教程(http://winterbe.com/posts/2014/03/16/java-8-tutorial/)。(java达人语:也可以阅读java lambda表达式)

Stream 如何工作

stream表示元素序列,并支持对这些元素进行不同类型的计算操作:

List<String> myList =
    Arrays.asList("a1", "a2", "b1", "c2", "c1");myList    .stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);// C1// C2

stream操作包括中间操作和终端操作。中间操作返回stream,这样我们就可以在不使用分号的情况下串联多个中间操作。终端操作返回void或者一个非stream结果值。在上面的示例中,filter, map 和 sorted 是中间操作,而forEach是一个终端操作。所有有关stream操作的完整列表,请参阅 Stream Javadoc(http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html)。如上例中所示的stream操作链也称为操作管道。

大多数stream操作接受某种lambda表达式参数,这是指定确切操作行为的函数接口。这些操作中的大多数必须是不干扰的,无状态的。那是什么意思?

当不修改stream的底层数据源时,该函数是不干扰的,例如,在上面的例子中,没有lambda表达式通过添加或删除集合中的元素来修改myList。

当操作的执行是确定的时候,函数是无状态的,例如,在上面的例子执行过程中,没有lambda表达式依赖于可能发生变化的外部作用域的任何可变变量或状态。

不同类型的stream

可以从各种数据源创建stream,特别是collections, List 和Set, 支持新方法 stream() 和 parallelStream(),以创建顺序或并行stream。并行stream可以在多个线程上运行,并将在本教程的后部分中介绍。我们现在关注顺序stream:

Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);  // a1

在对象list上调用方法 stream() 返回一个常规对象stream。但我们不需要创建集合来处理stream,如下例所示:

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

仅需要使用stream. of()从一堆对象引用中创建一个stream。

除了常规的对象stream,Java 8有特殊类型的stream,用于处理基本数据类型int,long和double。你可能已经猜到了,它是IntStream、LongStream和DoubleStream。

IntStreams可以使用IntStream.range()来代替常规的for循环。

IntStream.range(1, 4)
    .forEach(System.out::println);// 1// 2// 3

所有这些primitive stream都像普通对象stream一样,但有以下不同:原始stream使用专门的lambda表达式,例如是IntFunction而不是Function,是IntPredicate,而不是Predicate。primitive stream支持额外的终端聚合操作sum()和average():

Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println);  // 5.0

有时需要将一个普通对象stream转换为primitive stream,反之亦然。为此,对象stream支持专门的映射操作mapToInt()、mapToLong()和mapToDouble:

Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1))
    .mapToInt(Integer::parseInt)
    .max()
    .ifPresent(System.out::println);  // 3

原始stream可以通过mapToObj()转换为对象stream:

IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);// a1// a2// a3

这里有一个组合示例:double的stream首先映射到一个intstream,而不是映射到字符串的对象stream:

Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);// a1// a2// a3

处理顺序

现在我们已经了解了如何创建和处理不同类型的stream,让我们更深入地了解如何处理stream操作。

中间操作的一个重要特征是惰性。以下例子中,终端操作是缺失的:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    });

在执行此代码片段时,不会向控制台输出任何内容。这是因为中间操作只在出现终端操作时执行。

让我们通过终端操作forEach来扩展上面的例子:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));

执行这段代码片段会在控制台输出:

filter:  d2
forEach: d2
filter:  a2
forEach: a2
filter:  b1
forEach: b1
filter:  b3
forEach: b3
filter:  c
forEach: c

结果的输出顺序可能令人惊讶。一种简单的方法是在stream的所有元素上水平地执行操作。但此处相反,每个元素都沿着链垂直移动。第一个字符串“d2”先filter然后foreach,然后第二个字符串“a2”才被处理。

这种方式可以减少在每个元素上执行的实际操作数,如下例所示:

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .anyMatch(s -> {
        System.out.println("anyMatch: " + s);
        return s.startsWith("A");
    });// map:      d2// anyMatch: D2// map:      a2// anyMatch: A2

当predicate应用于给定的输入元素时,anyMatch将立即返回true。这对于第二个被传递的“A2”来说是正确的。由于stream链的垂直执行,在这种情况下,map只会执行两次。因此,map将尽可能少地被调用,而不是所有的元素映射到stream中。

为什么顺序很重要

下一个示例包括两个中间操作 map和filter和终端操作forEach。我们再一次查看这些操作是如何执行的:

Stream.of("d2", "a2", "b1", "b3", "c")
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("A");
    })
    .forEach(s -> System.out.println("forEach: " + s));// map:     d2// filter:  D2// map:     a2// filter:  A2// forEach: A2// map:     b1// filter:  B1// map:     b3// filter:  B3// map:     c// filter:  C

您可能已经猜到,底层集合中的每个字符串都被调用了5次map和filter,而forEach只调用一次。

如果我们改变操作的顺序,将filter移到链的开头,我们可以大大减少实际执行次数:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));// filter:  d2// filter:  a2// map:     a2// forEach: A2// filter:  b1// filter:  b3// filter:  c

现在,map只被调用一次,因此操作管道在大量元素输入时执行得更快。在编写复杂的方法链时,请记住这一点。

让我们通过一个额外的操作来扩展上面的示例,sorted:

Stream.of("d2", "a2", "b1", "b3", "c")
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2);
    })
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));

排序是一种特殊的中间操作。这是所谓的状态操作,因为要对元素进行排序,你需要维护元素的状态。

执行此示例将在控制台输出:

sort:    a2; d2
sort:    b1; a2
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2

首先,在整个输入集合上执行排序操作。换句话说, sorted是水平执行的。因此,在这个例子中,对输入集合中的每个元素进行多次组合, sorted被调用8次,。

我们再一次通过对链操作重排序来优化性能:

Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.startsWith("a");
    })
    .sorted((s1, s2) -> {
        System.out.printf("sort: %s; %s\n", s1, s2);
        return s1.compareTo(s2);
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    })
    .forEach(s -> System.out.println("forEach: " + s));// filter:  d2// filter:  a2// filter:  b1// filter:  b3// filter:  c// map:     a2// forEach: A2

在这个示例中,没有调用 sorted,因为filter将输入集合减少到一个元素。因此,对于大数据量的输入集合,性能会极大地提高。

Stream复用

Java 8 stream 无法复用。一旦你调用任何终端操作, stream就会关闭:

Stream<String> stream =
    Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> s.startsWith("a"));stream.anyMatch(s -> true);    // okstream.noneMatch(s -> true);   // exception

在同一条stream上的调用anyMatch之后调用noneMatch导致以下异常:

java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
    at com.winterbe.java8.Streams5.test7(Streams5.java:38)
    at com.winterbe.java8.Streams5.main(Streams5.java:28)

为了克服这个限制,必须为要执行的每一个终端操作创建一个新的stream链,例如,我们可以创建一个stream提供者来创建已构建所有中间操作的新stream:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));streamSupplier.get().anyMatch(s -> true);   // okstreamSupplier.get().noneMatch(s -> true);  // ok

每次调用get()构造一个新stream,我们在此调用终端操作。

java达人语:里面中间操作和终端操作的思想像极了spark中的RDD操作,也许了解java8 stream,是进入大数据的方便之门,请关注下期的文章,了解stream高级操作和并发stream。

原文发布于微信公众号 - java达人(drjava)

原文发表时间:2018-01-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java一日一条

为什么Java字符串是不可变对象?

本文主要来介绍一下Java中的不可变对象,以及Java中String类的不可变性,那么为什么Java的String类是不可变对象?让我们一起来分析一下。

9820
来自专栏韩伟的专栏

框架设计原则和规范(二)

此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,将分4天进行推送,今天推送4-5章。 1. 什么是好的框架 2. 框架设计...

31250
来自专栏C/C++基础

C++inline函数简介

inline函数是由inline关键字来定义,引入inline函数的主要原因是用它替代C中复杂易错不易维护的宏函数。

25420
来自专栏好好学java的技术栈

Java面试2018常考题目汇总

Linux起源于1991年,1995年流行起来的免费操作系统,目前, Linux是主流的服务器操作系统, 广泛应用于互联网、云计算、智能手机(Android)等...

13430
来自专栏Java社区

Python 自学步骤(文中有福利)

21740
来自专栏黑泽君的专栏

Java培训实战教程之Java基础知识精华部分(一)(二)(三)

10920
来自专栏wym

18年暑假多校赛第一场 1002

http://acm.hdu.edu.cn/showproblem.php?pid=6299

9910
来自专栏小灰灰

Java学习之深拷贝浅拷贝及对象拷贝的两种方式

I. Java之Clone 0. 背景 对象拷贝,是一个非常基础的内容了,为什么会单独的把这个领出来讲解,主要是先前遇到了一个非常有意思的场景 有一个任务,需要...

42490
来自专栏Java后端技术栈

Java性能优化之字符串优化处理

  String对象是Java中重要的数据类型,在大部分情况下我们都会用到String对象。其实在Java语言中,其设计者也对String做了大量的优化工作,这...

13030
来自专栏章鱼的慢慢技术路

《算法图解》第二章笔记与课后练习_选择排序算法

17530

扫码关注云+社区

领取腾讯云代金券