本文提供了有关Java 8 Stream的深刻概述。当我第一次读到的Stream API,我感到很困惑,由于它听起来相似Java I/O的InputStream,OutputStream。但Java 8 Stream是彻底不一样的东西。Streams是Monads,所以在为Java提供函数式编程方面发挥了重要做用:html
在函数式编程中,monad是表示定义为步骤序列的计算的结构。具备monad结构的类型定义链操做的含义,或将该类型的函数嵌套在一块儿。java
本文详解如何使用Java 8 Stream以及如何使用不一样类型的可用流操做。您将了解处理顺序以及流操做的顺序如何影响运行时性能。并对更强大的reduce,collect,flatMap流操做详细介绍。编程
若是您还不熟悉Java 8 lambda表达式,函数接口和方法引用,那么您可能须要了解Java 8。api
Stream表示一系列元素,并支持不一样类型的操做以对这些元素执行计算:oracle
List<String> streams =
Arrays.asList("a1", "a2", "b1", "c2", "c1");
streams
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
复制代码
以上代码的产出:less
C1
C2
复制代码
Stream操做是中间操做或终端操做。中间操做返回一个流,所以咱们能够连接多个中间操做而不使用分号。终端操做无效或返回非流结果。在上述例子中filter,map和sorted是中间操做,而forEach是一个终端的操做。有关全部可用流操做的完整列表,请参阅Stream Javadoc。如上例中所见的这种流操做链也称为操做管道。函数式编程
大多数流操做都接受某种lambda表达式参数,这是一个指定操做的确切行为的功能接口。大多数这些操做必须是不受干扰和无状态。函数
当函数不修改流的基础数据源时,该函数是不受干扰的,例如在上面的示例中,没有lambda表达式经过从集合中添加或删除元素来修改streams。性能
当操做的执行是肯定性的时,函数是无状态的,例如在上面的示例中,没有lambda表达式依赖于任何可变变量或来自外部做用域的状态,其可能在执行期间改变。优化
能够从各类数据源建立流,尤为是集合。Lists和Sets支持新的方法stream()
和parallelStream()
来建立顺序流或并行流。并行流可以在多个线程上操做,后面的部分将对此进行介绍。咱们如今关注的是顺序流:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
复制代码
以上代码的产出:
a1
复制代码
在对象列表上调用stream()
方法将返回常规对象流。可是咱们没必要建立集合以便使用流,就像咱们在下一个代码示例中看到的那样:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
复制代码
以上代码的产出:
a1
复制代码
只是用来Stream.of()
从一堆对象引用建立一个流。
除了常规对象流以外,Java 8还附带了特殊类型的流,用于处理原始数据类型int,long以及double。你可能已经猜到了IntStream
,LongStream
,DoubleStream
。
IntStreams可使用IntStream.range()
方法替换常规for循环:
IntStream.range(1, 4)
.forEach(System.out::println);
复制代码
以上代码的产出:
1
2
3
复制代码
全部这些原始流都像常规对象流同样工做,但有如下不一样之处:原始流使用专门的lambda表达式,例如IntFunction代替Function或IntPredicate代替Predicate。原始流支持额外的终端聚合操做,sum()
,average()
:
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println);
复制代码
以上代码的产出:
5.0
复制代码
有时将常规对象流转换为基本流是有用的,反之亦然。为此,对象流支持特殊的映射操做mapToInt()
,mapToLong()
,mapToDouble
:
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println);
复制代码
以上代码的产出:
3
复制代码
能够经过mapToObj()
方式将原始流转换为对象流:
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
复制代码
以上代码的产出:
a1
a2
a3
复制代码
下面是一个组合示例:双精度流首先映射到int流,而后映射到字符串的对象流:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
复制代码
以上代码的产出:
a1
a2
a3
复制代码
如今咱们已经学会了如何建立和使用不一样类型的流,让咱们深刻了解如何在流程下处理流操做。
中间操做的一个重要特征是懒惰。查看缺乏终端操做的示例:
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
复制代码
结果的顺序可能会使人惊讶。默认认为是在流的全部元素上一个接一个地水平执行操做。但相反,每一个元素都沿着链垂直移动。第一个字符串“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
复制代码
一旦谓词应用于给定的输入元素,anyMatch
操做将返回true。这对于传递给“A2”的第二个元素是正确的。因为流链的垂直执行,map
在这种状况下映射只需执行两次。所以,不是映射流的全部元素,而是map
尽量少地调用。
下一个示例包括两个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
复制代码
正如您可能已经猜到的,对于底层集合中的每一个字符串,map和filter都被调用5次,而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,对输入集合中的每一个元素的多个组合调用八次。
咱们能够经过从新排序链来优化性能:
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将输入集合减小到只有一个元素。所以,对于较大的输入集合,性能会大大提升。
Java 8 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
复制代码
在同一流上的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)
复制代码
为了克服这个限制,咱们必须为咱们想要执行的每一个终端操做建立一个新的流链,例如咱们能够建立一个流供应商来构建一个新的流,其中已经设置了全部中间操做:
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()构造一个咱们保存的新流,以调用所需的终端操做。