前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 8 Stream 教程 (一)

Java 8 Stream 教程 (一)

作者头像
java达人
发布2018-01-31 12:30:32
1.4K0
发布2018-01-31 12:30:32
举报
文章被收录于专栏:java达人java达人

作者: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表示元素序列,并支持对这些元素进行不同类型的计算操作:

代码语言:javascript
复制
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:

代码语言:javascript
复制
Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);  // a1

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

代码语言:javascript
复制
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循环。

代码语言:javascript
复制
IntStream.range(1, 4)
    .forEach(System.out::println);// 1// 2// 3

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

代码语言:javascript
复制
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:

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

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

代码语言:javascript
复制
IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);// a1// a2// a3

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

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

处理顺序

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

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

代码语言:javascript
复制
Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    });

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

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

代码语言:javascript
复制
Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> {
        System.out.println("filter: " + s);
        return true;
    })
    .forEach(s -> System.out.println("forEach: " + s));

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

代码语言:javascript
复制
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”才被处理。

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

代码语言:javascript
复制
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。我们再一次查看这些操作是如何执行的:

代码语言:javascript
复制
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移到链的开头,我们可以大大减少实际执行次数:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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));

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

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

代码语言:javascript
复制
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次,。

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

代码语言:javascript
复制
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就会关闭:

代码语言:javascript
复制
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导致以下异常:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-01-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档