作者: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); // ok
stream.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); // ok
streamSupplier.get().noneMatch(s -> true); // ok
每次调用get()构造一个新stream,我们在此调用终端操作。
java达人语:里面中间操作和终端操作的思想像极了spark中的RDD操作,也许了解java8 stream,是进入大数据的方便之门,请关注下期的文章,了解stream高级操作和并发stream。
java达人
ID:drjava
(扫码或长按识别)