上一章中描述和演示的 lambda 表达式以及函数接口为 Java 添加了强大的函数编程功能。它允许将行为(函数)作为参数传递给为数据处理性能而优化的库。通过这种方式,应用程序程序员可以专注于开发系统的业务方面,将性能方面留给专家:库的作者。这种库的一个例子是java.util.stream
包,这将是本章的重点。
我们将介绍数据流处理的概念,并解释什么是流,如何处理它们,以及如何构建处理管道。我们还将展示如何轻松地并行组织流处理。
本章将介绍以下主题:
- 什么是小溪?
- 创建流
- 中间业务
- 终端操作
- 流管道
- 并行处理
- 练习–将所有流元素相乘
理解流的最佳方法是将其与集合进行比较。后者是存储在内存中的数据结构。在将添加到集合之前,计算每个集合元素*。相反,流发射的元素存在于其他地方(在源中),并根据需要计算*。因此,集合可以是流的源。**
在 Java 中,流是java.util.stream
包的Stream
、IntStream
、LongStream
或DoubleStream
接口的对象。Stream
接口中的所有方法在IntStream
、LongStream
或DoubleStream
专用数字*流接口中也可用(有相应的类型变化)。一些数字流接口有一些特定于数值的额外方法,如average()
和sum()
。
在本章中,我们将主要讨论Stream
接口及其方法。但引入的所有内容也同样适用于数字流接口。在本章末尾,我们还将回顾一些在数字流接口中可用的方法,但在Stream
接口中不可用。
流表示某些数据源,例如集合、数组或文件,并在处理之前发出的元素后立即按顺序生成(生成、发出)一些值(与流类型相同的流元素)。
java.util.stream
包允许对过程(函数)进行声明性表示,这些过程(函数)也可以并行应用于发出的元素。今天,随着大规模数据处理的机器学习需求和操作的微调变得无处不在,这一特性加强了 Java 在为数不多的现代编程语言中的地位。
Stream
接口的许多方法(那些将函数接口类型作为参数的方法)称为操作,因为它们不是作为传统方法实现的。它们的功能作为函数传递到方法中。这些方法本身只是调用函数接口方法的 shell,被指定为方法参数的类型。
例如,让我们看看Stream<T> filter (Predicate<T> predicate)
方法。它的实现基于对Predicate<T>
函数的boolean test(T)
方法的调用。因此,程序员们不是说,“我们使用Stream
对象的filter()
方法来选择一些流元素并跳过其他元素”,而是说,“我们应用filter
操作,允许一些流元素通过,并跳过其他元素。”这听起来与“我们应用加法操作”的语句类似它描述动作(操作)的性质,而不是特定的算法,在方法接收到特定函数之前,该算法是未知的。
所以Stream
界面有三组方法:
- 创建
Stream
对象的静态工厂方法。 - 中间操作,即返回
Stream
对象的实例方法。 - 终端操作,返回除
Stream
以外的某种类型的实例方法。
流处理通常被组织为使用流畅(点连接)样式的管道(参见流管道部分)。Stream
工厂方法或其他流源启动此类管道,终端操作产生管道结果或副作用,并结束管道(因此,名称)。可以在原始Stream
对象和终端操作之间进行中间操作。它处理流元素(或在某些情况下不处理),并返回修改(或未修改)Stream
对象,因此可以应用下一个中间或终端操作。
中间操作的示例如下:
filter()
:仅选择符合条件的元素。map()
:根据函数转换元素。distinct()
:删除重复项。limit()
:这将流限制为指定数量的元素。sorted()
:将未排序的流转换为已排序的流。
我们将在中间操作部分讨论其他一些方法。
流元素的处理实际上仅在终端操作开始执行时开始。然后,所有中间操作(如果存在)开始处理。终端操作完成后,流立即关闭(并且无法重新打开)。终端操作的例子有forEach()
、findFirst()
、reduce()
、collect()
、sum()
、max()
等Stream
接口不返回Stream
的方法。我们将在终端操作部分讨论这些问题。
所有流方法都支持并行处理,这在多核计算机上处理大量数据的情况下尤其有用。必须确保处理管道不使用可能因不同处理环境而异的上下文状态。我们将在并行处理部分讨论这一点。
有很多方法可以创建一个流,Stream
类型的对象或任何数字接口。我们通过类和接口将它们分组,这些类和接口具有创建流对象的方法。我们这样做是为了方便读者,提供更好的概述,以便读者在需要时更容易找到它们。
这组Stream
工厂由属于Stream
接口的静态方法组成。
以下三种方法创建空或单个元素Stream
对象:
Stream<T> empty()
:创建一个空的顺序Stream
对象。Stream<T> of(T t)
:创建一个连续的单元素Stream
对象。Stream<T> ofNullable(T t)
:如果t
参数非空,则创建包含单个元素的顺序Stream
对象;否则,将创建一个空流。
以下代码演示了上述方法的用法:
Stream.empty().forEach(System.out::println); //prints nothing
Stream.of(1).forEach(System.out::println); //prints: 1
List<String> list = List.of("1 ", "2");
//printList1(null); //NullPointerException
printList1(list); //prints: 1 2
void printList1(List<String> list){
list.stream().forEach(System.out::print);;
}
注意,当列表不是null
时,对printList1()
方法的第一次调用如何生成NullPointerException
并打印1 2
。为了避免异常,我们可以按照如下方式实现printList1()
方法:
void printList1(List<String> list){
(list == null ? Stream.empty() : list.stream())
.forEach(System.out::print);
}
相反,我们使用了ofNullable(T t)
方法,正如您在printList2()
方法的以下实现中所看到的:
printList2(null); //prints nothing
printList2(list); //prints: [1 , 2]
void printList2(List<String> list){
Stream.ofNullable(list).forEach(System.out::print);
}
这就是促使创建ofNullable(T t)
方法的用例。但是您可能已经注意到,ofNullable()
将列表作为一个对象发送所创建的流:它被打印为[1 , 2]
。
在这种情况下,要处理列表中的每个元素,我们需要添加一个中间的Stream
操作flatMap()
,将每个元素转换为Stream
对象:
Stream.ofNullable(list).flatMap(e -> e.stream())
.forEach(System.out::print); //prints: 1 2
我们将在中间操作部分进一步讨论flatMap()
方法。
前面代码中传递到flatMap()
操作的函数也可以表示为方法引用:
Stream.ofNullable(list).flatMap(Collection::stream)
.forEach(System.out::print); //prints: 1 2
Stream
接口的两种静态方法允许我们使用类似于传统for
循环的迭代过程生成值流:
Stream<T> iterate(T seed, UnaryOperator<T> func)
:基于第二个参数(一个func
函数)对第一个seed
参数的迭代应用,创建一个无限顺序Stream
对象,产生seed
、f(seed)
和f(f(seed))
值流。Stream<T> iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> next)
:基于第三个参数next
函数对第一个seed
参数的迭代应用,创建一个有限的顺序Stream
对象,只要第三个参数hasNext
函数返回true
值,就产生一个seed
、f(seed)
和f(f(seed))
值流。
以下代码演示了这些方法的用法:
Stream.iterate(1, i -> ++i).limit(9)
.forEach(System.out::print); //prints: 123456789
Stream.iterate(1, i -> i < 10, i -> ++i)
.forEach(System.out::print); //prints: 123456789
请注意,我们被迫向第一个管道添加一个limit()
中间运算符,以避免生成无限多的值。
Stream
接口的Stream<T>
concatenate(Stream<> a
、Stream<T> b
静态方法基于作为参数传入的两个流对象a
和b
创建一个值流。新创建的流包括第一个参数a
的所有元素,然后是第二个参数b
的所有元素。以下代码演示了创建流对象的此方法:
Stream<Integer> stream1 = List.of(1, 2).stream();
Stream<Integer> stream2 = List.of(2, 3).stream();
Stream.concat(stream1, stream2)
.forEach(System.out::print); //prints: 1223
请注意,2
元素在两个原始流中都存在,因此,它在结果流中存在两次。
Stream
接口的Stream<T> generate(Supplier<T> supplier)
静态方法创建一个无限流,其中每个元素由提供的Supplier<T>
函数生成。以下是两个例子:
Stream.generate(() -> 1).limit(5)
.forEach(System.out::print); //prints: 11111
Stream.generate(() -> new Random().nextDouble()).limit(5)
.forEach(System.out::println); //prints: 0.38575117472619247
// 0.5055765386778835
// 0.6528038976983277
// 0.4422354489467244
// 0.06770955839148762
由于流是无限的,我们添加了一个limit()
操作。
Stream<T> of(T... values)
方法接受 varargs 或一个值数组,并使用提供的值作为流元素创建流对象:
Stream.of("1 ", 2).forEach(System.out::print); //prints: 1 2
//Stream<String> stringStream = Stream.of("1 ", 2); //compile error
String[] strings = {"1 ", "2"};
Stream.of(strings).forEach(System.out::print); //prints: 1 2
注意,在前面代码的第一行,Stream
对象接受不同类型的元素,如果Stream
引用声明的泛型中没有指定类型。在下一行中,泛型将Stream
对象的类型定义为String
,相同的元素类型组合会生成编译错误。泛型确实可以帮助程序员避免许多错误,并且应该尽可能地使用泛型。
of(T... values)
方法也可用于多个流的串联。例如,让我们假设我们有以下四个流,并且我们希望连接成一个流:
Stream<Integer> stream1 = Stream.of(1, 2);
Stream<Integer> stream2 = Stream.of(2, 3);
Stream<Integer> stream3 = Stream.of(3, 4);
Stream<Integer> stream4 = Stream.of(4, 5);
我们期望新流发出值1
、2
、2
、3
、3
、4
、4
和5
。首先,我们尝试以下代码:
Stream.of(stream1, stream2, stream3, stream4)
.forEach(System.out::print);
//prints: java.util.stream.ReferencePipeline$Head@58ceff1j
前面的代码没有达到我们希望的效果。它将每个流视为java.util.stream.ReferencePipeline
内部类的对象,该类用于Stream
接口实现。因此,我们添加了一个将每个流元素转换为流的flatMap()
操作(我们将在中间操作一节中描述):
Stream.of(stream1, stream2, stream3, stream4)
.flatMap(e -> e).forEach(System.out::print); //prints: 12233445
我们作为参数(e -> e
传入flatMap()
的函数可能看起来好像什么都没做,但这是因为流的每个元素都已经是流了,所以我们不需要对其进行转换。通过返回一个元素作为flatMap()
操作的结果,我们已经告诉管道将其视为Stream
对象。完成后,将显示预期结果。
Stream.Builder<T> builder()
静态方法返回一个内部(位于接口Stream
接口内部)Builder
接口,可用于构造Stream
对象。Builder
接口扩展了Consumer
接口,有以下几种方式:
void accept(T t)
:向流中添加元素(此方法来自Consumer
接口)。default Stream.Builder<T> add(T t)
:调用accept(T)
方法并返回this
,从而允许以流畅、点连接的方式链接add(T)
方法。Stream<T> build()
:将此生成器从构建状态转换为构建状态。调用此方法后,不能向流中添加新元素。
使用add()
方法很简单:
Stream.<String>builder().add("cat").add(" dog").add(" bear")
.build().forEach(System.out::print); //prints: cat dog bear
请注意我们在builder()
方法前面添加的<String>
泛型。这样,我们告诉构建器我们正在创建的流将具有String
类型的元素。否则,将它们添加为Object
类型。
当生成器作为Consumer
类型的参数传递时,或者当您不需要链接添加元素的方法时,使用accept()
方法。例如,以下是构建器作为Consumer
对象传入的方式:
Stream.Builder<String> builder = Stream.builder();
List.of("1", "2", "3").stream().forEach(builder);
builder.build().forEach(System.out::print); //prints: 123
还有一些情况下,在添加流元素时不需要链接方法。以下方法接收String
对象列表,并将其中一些(包含字符a
的对象)添加到流中:
Stream<String> buildStream(List<String> values){
Stream.Builder<String> builder = Stream.builder();
for(String s: values){
if(s.contains("a")){
builder.accept(s);
}
}
return builder.build();
}
请注意,出于相同的原因,我们在Stream.Builder
接口中添加了<String>
泛型,以告知构建器我们要添加的元素应被视为String
类型。
调用前面的方法时,它将生成预期结果:
List<String> list = List.of("cat", " dog", " bear");
buildStream(list).forEach(System.out::print); //prints: cat bear
在 Java 8 中,java.util.Collection
接口增加了两个默认方法:
Stream<E> stream()
:返回此集合的元素流。Stream<E> parallelStream()
:返回(可能)此集合元素的并行流。我们之所以这样说,可能是因为 JVM 试图将流分割成几个块,并并行(如果有多个 CPU)或虚拟并行(使用 CPU 的分时)处理它们。这并不总是可能的;这部分取决于请求处理的性质。
这意味着扩展此接口的所有采集接口,包括Set
和List
都有这些方法。以下是一个例子:
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.stream().forEach(System.out::print); //prints: 12345
我们将在并行处理部分进一步讨论并行流。
java.util.Arrays
类中也添加了八个静态重载stream()
方法。它们从相应的数组或其子集创建不同类型的流:
Stream<T> stream(T[] array)
:从提供的数组创建Stream
。IntStream stream(int[] array)
:从提供的数组创建IntStream
。LongStream stream(long[] array)
:从提供的数组创建LongStream
。DoubleStream stream(double[] array)
:从提供的数组创建DoubleStream
。Stream<T> stream(T[] array, int startInclusive, int endExclusive)
:从所提供数组的指定范围创建Stream
。IntStream stream(int[] array, int startInclusive, int endExclusive)
:从所提供数组的指定范围创建IntStream
。LongStream stream(long[] array, int startInclusive, int endExclusive)
:从所提供数组的指定范围创建LongStream
。DoubleStream stream(double[] array, int startInclusive, int endExclusive)
:从所提供数组的指定范围创建DoubleStream
。
以下是从数组子集创建流的示例:
int[] arr = {1, 2, 3, 4, 5};
Arrays.stream(arr, 2, 4).forEach(System.out::print); //prints: 34
请注意,我们使用了Stream<T> stream(T[] array, int startInclusive, int endExclusive)
方法,这意味着我们创建了Stream
而不是IntStream
,尽管创建的流中的所有元素都是整数,如IntStream
中所示。不同之处在于IntStream
提供了一些Stream
中没有的数字特定操作(参见数字流接口部分)。
java.util.Random
类允许我们创建伪随机值的数字流:
IntStream ints()
和LongStream longs()
:创建对应类型的无限伪随机值流。DoubleStream doubles()
:创建一个不受限制的伪随机双值流,每个值介于零(包括)和一(排除)之间。IntStream ints(long streamSize)
和LongStream longs(long streamSize)
:创建对应类型的指定数量的伪随机值流。DoubleStream doubles(long streamSize)
:创建指定数量的伪随机双精度值流,每个值介于零(包括)和一(排除)之间。IntStream ints(int randomNumberOrigin, int randomNumberBound)
、LongStream longs(long randomNumberOrigin, long randomNumberBound)
、DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound)
:创建对应类型的无限伪随机值流,每个值等于或大于第一个参数,小于第二个参数。
以下是上述方法之一的示例:
new Random().ints(5, 8)
.limit(5)
.forEach(System.out::print); //prints: 56757
java.nio.File
类有六个静态方法来创建线和路径流:
Stream<String> lines(Path path)
:根据提供的路径指定的文件创建行流。Stream<String> lines(Path path, Charset cs)
:根据提供的路径指定的文件创建行流。使用提供的字符集将文件中的字节解码为字符。Stream<Path> list(Path dir)
:在指定目录中创建一个条目流。Stream<Path> walk(Path start, FileVisitOption... options)
:创建以给定起始文件为根的文件树条目流。Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options)
:创建以给定起始文件为根、指定深度的文件树条目流。Stream<Path> find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
:创建以给定起始文件为根的文件树条目流,其深度与提供的谓词匹配。
创建流的其他类和方法包括:
java.util.BitSet
类的IntStream stream()
:创建一个索引流,其中BitSet
包含一个处于设置状态的位。java.io.BufferedReader
类的Stream<String> lines()
:创建从BufferedReader
对象读取的行流,通常从文件读取。java.util.jar.JarFile
类的Stream<JarEntry> stream()
:创建 ZIP 文件条目流。java.lang.CharSequence
接口的默认IntStream chars()
:创建一个int
零流,扩展此序列中的char
值。java.lang.CharSequence
接口的默认IntStream codePoints()
:根据该序列创建一个代码点值流。java.util.regex.Pattern
类的Stream<String> splitAsStream(CharSequence input)
:根据提供的序列围绕此模式的匹配创建流。
还有java.util.stream.StreamSupport
类,它包含库开发人员使用的静态低级实用程序方法。这超出了本书的范围。
我们已经看到了如何创建表示源并发射元素的Stream
对象。如前所述,Stream
接口提供的操作(方法)可分为三组:
- 基于源创建
Stream
对象的方法。 - 接受函数并生成发出相同或修改值的
Stream
对象的中间操作。 - 完成流处理、关闭流并生成结果的终端操作。
在本节中,我们将回顾可按功能分组的中间操作。
此组包括删除重复项、跳过某些元素和限制已处理元素数量的操作,仅选择需要的元素:
Stream<T> distinct()
:使用Object.equals(Object)
方法比较流元素,并跳过重复项。Stream<T> skip(long n)
:忽略提供的首先发出的流元素数量。Stream<T> limit(long maxSize)
:只允许处理提供数量的流元素。Stream<T> filter(Predicate<T> predicate)
:仅允许产生true
的元素(当由提供的Predicate
功能处理时)。- 默认
Stream<T> dropWhile(Predicate<T> predicate)
:在所提供的Predicate
功能处理时,跳过导致true
的流的第一个元素。 - 默认
Stream<T> takeWhile(Predicate<T> predicate)
:只允许处理产生true
的流的第一个元素(当由提供的Predicate
函数处理时)。
以下代码演示了上述操作的工作原理:
Stream.of("3", "2", "3", "4", "2").distinct()
.forEach(System.out::print); //prints: 324
List<String> list = List.of("1", "2", "3", "4", "5");
list.stream().skip(3).forEach(System.out::print); //prints: 45
list.stream().limit(3).forEach(System.out::print); //prints: 123
list.stream().filter(s -> Objects.equals(s, "2"))
.forEach(System.out::print); //prints: 2
list.stream().dropWhile(s -> Integer.valueOf(s) < 3)
.forEach(System.out::print); //prints: 345
list.stream().takeWhile(s -> Integer.valueOf(s) < 3)
.forEach(System.out::print); //prints: 12
请注意,我们可以重用List<String>
源对象,但不能重用Stream
对象。一旦关闭,就无法重新打开。
这一组可能包括最重要的中间操作。它们是修改流元素的唯一中间操作。它们将原始流元素值映射(转换)为新的流元素值:
Stream<R> map(Function<T, R> mapper)
:将提供的函数应用于该流T
类型的每个元素,并生成R
类型的新元素值。IntStream mapToInt(ToIntFunction<T> mapper)
:将此流转换为Integer
值中的IntStream
。LongStream mapToLong(ToLongFunction<T> mapper)
:将此流转换为Long
值中的LongStream
。DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
:将此流转换为Double
值中的DoubleStream
。Stream<R> flatMap(Function<T, Stream<R>> mapper)
:将提供的函数应用于该流的T
类型的每个元素,并生成一个Stream<R>
对象,该对象发射R
类型的元素。IntStream flatMapToInt(Function<T, IntStream> mapper)
:使用提供的函数将T
类型的每个元素转换为Integer
值流。LongStream flatMapToLong(Function<T, LongStream> mapper)
:使用提供的函数将T
类型的每个元素转换为Long
值流。DoubleStream flatMapToDouble(Function<T, DoubleStream> mapper)
:使用提供的函数将T
类型的每个元素转换为Double
值流。
以下是使用这些操作的示例:
List<String> list = List.of("1", "2", "3", "4", "5");
list.stream().map(s -> s + s)
.forEach(System.out::print); //prints: 1122334455
list.stream().mapToInt(Integer::valueOf)
.forEach(System.out::print); //prints: 12345
list.stream().mapToLong(Long::valueOf)
.forEach(System.out::print); //prints: 12345
list.stream().mapToDouble(Double::valueOf)
.mapToObj(Double::toString)
.map(s -> s + " ")
.forEach(System.out::print);//prints: 1.0 2.0 3.0 4.0 5.0
list.stream().mapToInt(Integer::valueOf)
.flatMap(n -> IntStream.iterate(1, i -> i < n, i -> ++i))
.forEach(System.out::print); //prints: 1121231234
list.stream().map(Integer::valueOf)
.flatMapToInt(n ->
IntStream.iterate(1, i -> i < n, i -> ++i))
.forEach(System.out::print); //prints: 1121231234
list.stream().map(Integer::valueOf)
.flatMapToLong(n ->
LongStream.iterate(1, i -> i < n, i -> ++i))
.forEach(System.out::print); //prints: 1121231234;
list.stream().map(Integer::valueOf)
.flatMapToDouble(n ->
DoubleStream.iterate(1, i -> i < n, i -> ++i))
.mapToObj(Double::toString)
.map(s -> s + " ")
.forEach(System.out::print);
//prints: 1.0 1.0 2.0 1.0 2.0 3.0 1.0 2.0 3.0 4.0
在前面的示例中,对于Double
值,我们将一个数值转换为String
,并添加了空格,因此结果将以数字之间的空格打印。这些示例非常简单,只需最少的处理即可进行转换。但在现实生活中,每一个map
或flatMap
操作都可以接受一个(任何复杂程度的函数)来做一些真正有用的事情。
以下两个中间操作对流元素进行排序。当然,在所有元素发出之前,此类操作无法完成,因此它会产生大量开销,降低性能,并且必须用于小规模流:
Stream<T> sorted()
:按自然顺序对流元素进行排序(根据其Comparable
接口实现)。Stream<T> sorted(Comparator<T> comparator)
:根据提供的Comparator<T>
对象对流元素进行排序。
以下是演示代码:
List<String> list = List.of("2", "1", "5", "4", "3");
list.stream().sorted().forEach(System.out::print); //prints: 12345
list.stream().sorted(Comparator.reverseOrder())
.forEach(System.out::print); //prints: 54321
Stream<T> peek(Consumer<T> action)
中间操作将提供的Consumer
函数应用于每个流元素,并且不会更改此Stream
(返回其已接收到的相同元素值),因为Consumer
函数返回void
且不会影响该值。此操作用于调试。
以下代码显示了它的工作原理:
List<String> list = List.of("1", "2", "3", "4", "5");
list.stream().peek(s-> {
if("3".equals(s)){
System.out.print(3);
}
}).forEach(System.out::print); //prints: 123345
终端操作是流管道最重要的操作。无需任何其他操作即可轻松完成所有操作。我们已经使用了forEach(Consumer<T>)
终端操作来打印每个元素。它不返回值;因此,它被用于其副作用。但是Stream
接口有许多更强大的终端操作,它们返回值。其中最核心的是collect()
手术,有R collect(Collector<T, A, R> collector)
和R collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)
两种形式。这使我们能够组合几乎任何可以应用于流的流程。经典的例子如下:
List<String> asList = stringStream.collect(ArrayList::new,
ArrayList::add,
ArrayList::addAll);
如您所见,它是以适合并行处理的方式实现的。它使用第一个函数根据流元素生成一个值,使用第二个函数累积结果,然后合并处理流的所有线程的累积结果。
然而,只有一个这样的通用终端操作将迫使程序员重复编写相同的函数。这就是 API 作者添加Collectors
类的原因,该类生成许多专门的Collector
对象,而无需为每个collect()
操作创建三个函数。除此之外,API 作者还添加了更专门的终端操作,这些操作在Stream
接口上更简单、更易于使用。
在本节中,我们将回顾Stream
接口的所有终端操作,并在Collecting
小节中,查看Collectors
类生成的大量Collector
对象。
我们将从最简单的终端操作开始,它允许一次处理一个流的每个元素。
此组中有两个终端操作:
void forEach(Consumer<T> action)
:为流的每个元素应用提供的操作(流程)。void forEachOrdered(Consumer<T> action)
:按照源定义的顺序为流的每个元素应用提供的操作(流程),无论流是顺序的还是并行的。
如果需要处理元素的顺序对应用程序很重要,并且必须按照源代码中值的排列顺序,请使用第二种方法,特别是如果可以预见代码将在具有多个 CPU 的计算机上执行。否则,使用第一个,正如我们在所有示例中所做的那样。
在任何类型的流处理中使用此操作并不罕见,尤其是当代码是由经验不足的程序员编写时。对于以下示例,我们创建了Person
类:
class Person {
private int age;
private String name;
public Person(int age, String name) {
this.name = name;
this.age = age;
}
public String getName() { return this.name; }
public int getAge() {return this.age; }
@Override
public String toString() {
return "Person{" + "name='" + this.name + "'" +
", age=" + age + "}";
}
}
我们将在终端操作的整个讨论中使用这节课。在本例中,我们将从文件中读取逗号分隔的值(年龄和名称),并创建Person
对象。我们已将以下persons.csv
文件(**逗号分隔值(CSV)**放置在resources
文件夹中:
23 , Ji m
2 5 , Bob
15 , Jill
17 , Bi ll
请注意我们在值的外部和内部添加的空格。我们这样做是为了借此机会向您展示一些处理真实数据的简单但非常有用的技巧。下面是一个没有经验的程序员如何编写代码来读取此文件并创建一个Person
对象列表:
List<Person> persons = new ArrayList<>();
Path path = Paths.get("src/main/resources/persons.csv");
try (Stream<String> lines = Files.newBufferedReader(path).lines()) {
lines.forEach(s -> {
String[] arr = s.split(",");
int age = Integer.valueOf(StringUtils.remove(arr[0], ' '));
persons.add(new Person(age, StringUtils.remove(arr[1], ' ')));
});
} catch (IOException ex) {
ex.printStackTrace();
}
persons.stream().forEach(System.out::println);
//prints: Person{name='Jim', age=23}
// Person{name='Bob', age=25}
// Person{name='Jill', age=15}
// Person{name='Bill', age=17}
您可以看到,我们使用了String
方法split()
,用逗号分隔每一行的值,并且我们使用org.apache.commons.lang3.StringUtils
类从每个值中删除空格。前面的代码还提供了一个真实的try-with-resources
构造示例,用于自动关闭BufferedReader
对象。
尽管此代码在小型示例和单核计算机上运行良好,但它可能会在长流和并行处理中产生意外的结果。也就是说,lambda 表达式要求所有变量都是 final,或者实际上是 final,因为同一个函数可以在不同的上下文中执行。
相比之下,以下是上述代码的正确实现:
List<Person> persons = new ArrayList<>();
Path path = Paths.get("src/main/resources/persons.csv");
try (Stream<String> lines = Files.newBufferedReader(path).lines()) {
persons = lines.map(s -> s.split(","))
.map(arr -> {
int age = Integer.valueOf(StringUtils.remove(arr[0], ' '));
return new Person(age, StringUtils.remove(arr[1], ' '));
}).collect(Collectors.toList());
} catch (IOException ex) {
ex.printStackTrace();
}
persons.stream().forEach(System.out::println);
为了提高可读性,可以创建一个执行映射工作的方法:
public List<Person> createPersons() {
List<Person> persons = new ArrayList<>();
Path path = Paths.get("src/main/resources/persons.csv");
try (Stream<String> lines = Files.newBufferedReader(path).lines()) {
persons = lines.map(s -> s.split(","))
.map(this::createPerson)
.collect(Collectors.toList());
} catch (IOException ex) {
ex.printStackTrace();
}
return persons;
}
private Person createPerson(String[] arr){
int age = Integer.valueOf(StringUtils.remove(arr[0], ' '));
return new Person(age, StringUtils.remove(arr[1], ' '));
}
如您所见,我们使用了collect()
操作和Collectors.toList()
方法创建的Collector
函数。我们将在Collect小节中看到Collectors
类创建的更多Collector
函数。
Stream
接口的long count()
终端操作看起来简单、良性。它返回此流中的元素数。那些习惯于使用集合和数组的人可以不用三思而后行地使用count()
操作。以下是一个证明其工作正常的示例:
long count = Stream.of("1", "2", "3", "4", "5")
.peek(System.out::print)
.count();
System.out.print(count); //prints: 5
如您所见,实现方法 count 的代码能够在不执行所有管道的情况下确定流大小。peek()
操作没有打印元素值,这证明元素没有发出。但并非总是能够在源位置确定流的大小。此外,这股水流可能是无限的。因此,必须小心使用count()
。
由于我们的主题是计算元素,我们想向您展示另一种可能的方法来确定流大小,使用collect()
操作:
int count = Stream.of("1", "2", "3", "4", "5")
.peek(System.out::print) //prints: 12345
.collect(Collectors.counting());
System.out.println(count); //prints: 5
您可以看到,collect()
操作的实现甚至没有尝试计算源处的流大小(因为,正如您所看到的,管道已完全执行,每个元素都由peek()
操作打印)。这是因为collect()
操作没有count()
操作那么专业。它只将传入的收集器应用于流,收集器对collect()
操作提供给它的元素进行计数。你可以认为这是一个官僚近视的例子:每一个操作员都像预期的那样工作,但是总体性能是不理想的。
有三种(看起来非常相似)终端操作,允许我们评估所有、任何或任何流元素是否具有特定值:
boolean allMatch(Predicate<T> predicate)
:当每个流元素返回true
时,返回true
,当用作所提供的Predicate<T>
函数的参数时boolean anyMatch(Predicate<T> predicate)
:当其中一个流元素返回true
时,返回true
,该流元素用作所提供的Predicate<T>
函数的参数boolean noneMatch(Predicate<T> predicate)
:当所有流元素都不返回true
时,返回true
,用作提供的Predicate<T>
函数的参数。
以下是它们的用法示例:
List<String> list = List.of("1", "2", "3", "4", "5");
boolean found = list.stream()
.peek(System.out::print) //prints: 123
.anyMatch(e -> "3".equals(e));
System.out.print(found); //prints: true <= line 5
found = list.stream()
.peek(System.out::print) //prints: 12345
.anyMatch(e -> "0".equals(e));
System.out.print(found); //prints: false
boolean noneMatches = list.stream()
.peek(System.out::print) //prints: 123
.noneMatch(e -> "3".equals(e));
System.out.print(noneMatches); //prints: false
noneMatches = list.stream()
.peek(System.out::print) //prints: 12345
.noneMatch(e -> "0".equals(e));
System.out.print(noneMatches); //prints: true <= line 17
boolean allMatch = list.stream()
.peek(System.out::print) //prints: 1
.allMatch(e -> "3".equals(e));
System.out.print(allMatch); //prints: false
让我们更仔细地看一下前面示例的结果。这些操作中的每一个都会触发流管道执行,并且每次至少处理流的一个元素。但是看看anyMatch()
和noneMatch()
操作。第 5 行说明至少有一个元素等于3
。仅处理了前三个元素后,返回结果*。第 17 行指出,在流的所有元素都被处理之后,没有任何元素等于0
。*
问题是,当您想知道流是否不包含的v
值时,您应该使用这两个操作中的哪一个?如果使用了noneMatch()
,则所有元素都将被处理。但是如果使用了anyMatch()
,则只有当流中没有v
值时,所有元素才会被处理。noneMatch()
操作似乎没用,因为anyMatch()
返回true
时,与noneMatch()
返回false
时的含义相同,而anyMatch()
操作只需处理较少的元素即可实现。这种差异随着流大小的增加和存在具有v
值的元素的可能性而显著增加。在处理时间不重要的情况下,执行noneMatch()
操作的唯一原因似乎是为了代码可读性,因为流的大小很小。
allMatch()
操作没有替代方法,与anyMatch()
类似,要么在遇到第一个不匹配元素时返回,要么需要处理所有流元素。
以下终端操作允许我们查找流的任何或第一个元素:
Optional<T> findAny()
:返回包含流中任意元素值的Optional
,如果流为空则返回空Optional
。Optional<T> findFirst()
:返回流的第一个元素的值Optional
,如果流为空,则返回空Optional
。
以下示例说明了这些操作:
List<String> list = List.of("1", "2", "3", "4", "5");
Optional<String> result = list.stream().findAny();
System.out.println(result.isPresent()); //prints: true
System.out.println(result.get()); //prints: 1
result = list.stream().filter(e -> "42".equals(e)).findAny();
System.out.println(result.isPresent()); //prints: true
//System.out.println(result.get()); //NoSuchElementException
result = list.stream().findFirst();
System.out.println(result.isPresent()); //prints: true
System.out.println(result.get()); //prints: 1
如您所见,它们返回相同的结果。这是因为我们在单个线程中执行管道。这两种操作之间的差异在并行处理中更为突出。当流被分成几个部分进行并行处理时,findFirst()
操作总是返回流的第一个元素(如果流不是空的),而findAny()
操作只返回其中一个处理线程中的第一个元素。
让我们更详细地讨论java.util.Optional
课程。
java.util.Optional
的对象用于避免返回null
,因为它可能导致NullPointerException
。相反,Optional
对象提供的方法可用于检查值是否存在,并在没有值时替换它。例如:
List<String> list = List.of("1", "2", "3", "4", "5");
String result = list.stream().filter(e -> "42".equals(e))
.findAny().or(() -> Optional.of("Not found")).get();
System.out.println(result); //prints: Not found
result = list.stream().filter(e -> "42".equals(e))
.findAny().orElse("Not found");
System.out.println(result); //prints: Not found
Supplier<String> trySomethingElse = () -> {
//Code that tries something else
return "43";
};
result = list.stream().filter(e -> "42".equals(e))
.findAny().orElseGet(trySomethingElse);
System.out.println(result); //prints: 43
list.stream().filter(e -> "42".equals(e))
.findAny().ifPresentOrElse(System.out::println,
() -> System.out.println("Not found")); //prints: Not found
如您所见,如果Optional
对象为空,则:
Optional
类的or()
方法允许返回另一个Optional
对象(带值)。orElse()
方法允许返回替代值。orElseGet()
方法允许提供Supplier
函数,该函数返回一个可选值。ifPresentOrElse()
方法允许提供两个功能:一个使用Optional
对象的值,另一个在Optional
对象为空时执行操作。
以下终端操作返回流元素的最小值或最大值(如果存在):
Optional<T> min
(比较器比较器):使用提供的比较器对象返回此流的最小元素。Optional<T> max
(比较器比较器):使用提供的比较器对象返回此流的最大元素。
下面是演示代码:
List<String> list = List.of("a", "b", "c", "c", "a");
String min = list.stream().min(Comparator.naturalOrder()).orElse("0");
System.out.println(min); //prints: a
String max = list.stream().max(Comparator.naturalOrder()).orElse("0");
System.out.println(max); //prints: c
如您所见,在非数值的情况下,根据提供的比较器,最小元素是第一个元素(从左到右排序);因此,最大值是最后一个元素。对于数值,最小值和最大值只是流元素中的最大值和最小值:
int mn = Stream.of(42, 33, 77).min(Comparator.naturalOrder()).orElse(0);
System.out.println(mn); //prints: 33
int mx = Stream.of(42, 33, 77).max(Comparator.naturalOrder()).orElse(0);
System.out.println(mx); //prints: 77
让我们看另一个例子,假设有一个Person
类:
class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age + "}";
}
}
任务是在以下列表中查找最年长的人:
List<Person> persons = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
为此,我们可以创建以下Compartor<Person>
:
Comparator<Person> perComp = (p1, p2) -> p1.getAge() - p2.getAge();
然后,使用这个比较器,我们可以找到最年长的人:
Person theOldest = persons.stream().max(perComp).orElse(null);
System.out.println(theOldest); //prints: Person{name:Jim,age:33}
这两个终端操作生成一个包含流元素的数组:
Object[] toArray()
:创建一个对象数组;每个对象都是此流的一个元素。A[] toArray(IntFunction<A[]> generator)
:使用提供的函数创建流元素数组。
让我们看一个例子:
List<String> list = List.of("a", "b", "c");
Object[] obj = list.stream().toArray();
Arrays.stream(obj).forEach(System.out::print); //prints: abc
String[] str = list.stream().toArray(String[]::new);
Arrays.stream(str).forEach(System.out::print); //prints: abc
第一个例子很简单。它将元素转换为相同类型的数组。至于第二个例子,IntFunction
作为String[]::new
的表示可能并不明显,所以让我们来看看它。
String[]::new
是表示以下 lambda 表达式的方法引用:
String[] str = list.stream().toArray(i -> new String[i]);
Arrays.stream(str).forEach(System.out::print); //prints: abc
这已经是IntFunction<String[]>
,根据其文档,它接受int
参数并返回指定类型的结果。可以使用匿名类定义它,如下所示:
IntFunction<String[]> intFunction = new IntFunction<String[]>() {
@Override
public String[] apply(int i) {
return new String[i];
}
};
您可能还记得(从第 13 章、Java 集合中)我们是如何将集合转换为数组的:
str = list.toArray(new String[list.size()]);
Arrays.stream(str).forEach(System.out::print); //prints: abc
您可以看到,Stream
接口的toArray()
操作具有非常相似的签名,只是它接受一个函数,而不仅仅是一个数组。
此终端操作称为reduce,因为它处理所有流元素并生成一个值。它将所有流元素减少为一个值。但这并不是唯一能做到这一点的行动。collect操作也将流元素的所有值减少为一个结果。而且,在某种程度上,所有的终端操作都会减少。它们在处理所有元素后产生一个值。
因此,您可以将reduce和collect视为同义词,它们有助于为Stream
界面中的许多可用操作添加结构和分类。此外,reduce组中的操作可以被视为collect操作的专用版本,因为collect()
也可以被定制以提供相同的功能。
有了这些,我们来看一组减少操作:
Optional<T> reduce(BinaryOperator<T> accumulator)
:通过使用提供的定义元素聚合逻辑的关联函数来减少此流的元素。返回带缩减值的Optional
,如果可用。T reduce(T identity, BinaryOperator<T> accumulator)
:提供与先前reduce()
版本相同的功能,但将identity
参数用作累加器的初始值,如果流为空,则为默认值。U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
:提供与之前reduce()
版本相同的功能,但在将此操作应用于并行流时,使用combiner
功能聚合结果。如果流不是并行的,则不使用组合器功能。
为了演示reduce()
操作,我们将使用与之前相同的Person
类:
class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() { return this.age; }
public String getName() { return this.name; }
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + this.age + "}";
}
}
我们还将使用与流示例源相同的Person
对象列表:
List<Person> list = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
使用reduce()
这个操作,现在让我们找到最老的人:
Person theOldest = list.stream()
.reduce((p1, p2) -> p1.getAge() > p2.getAge() ? p1 : p2).orElse(null);
System.out.println(theOldest); //prints: Person{name:Jim,age:33}
实现有点令人惊讶,不是吗?我们谈论的是“累加器”,但我们没有积累任何东西。我们只是比较了所有的流元素。显然,累加器保存比较结果,并将其作为下一次比较(与下一个元素)的第一个参数提供。在本例中,可以说累加器将所有先前比较的结果累加起来。无论如何,它做了我们希望它做的工作。
现在让我们明确地积累一些东西。让我们将列表中的所有姓名合并到一个逗号分隔的列表中:
String allNames = list.stream().map(p->p.getName())
.reduce((n1, n2) -> n1 + ", " + n2).orElse(null);
System.out.println(allNames); //prints: Bob, Jim, Jill, Bill
在这种情况下,积累的概念更有意义,不是吗?
现在,让我们使用标识值来提供初始值:
String allNames = list.stream().map(p->p.getName())
.reduce("All names: ", (n1, n2) -> n1 + ", " + n2);
System.out.println(allNames); //All names: , Bob, Jim, Jill, Bill
注意,这个版本的reduce()
操作返回的是值,而不是Optional
对象。这是因为,通过提供初始值,我们保证该值将出现在结果中,即使流结果为空。
但是结果字符串看起来并不像我们希望的那样漂亮。显然,所提供的初始值被视为任何其他流元素,并且我们创建的累加器会在其后面添加一个逗号。为了使结果看起来更漂亮,我们可以再次使用第一版的reduce()
操作,并通过以下方式添加初始值:
String allNames = "All names: " + list.stream().map(p->p.getName())
.reduce((n1, n2) -> n1 + ", " + n2).orElse(null);
System.out.println(allNames); //All names: Bob, Jim, Jill, Bill
出于演示目的,我们决定使用空格作为分隔符,而不是逗号:
String allNames = list.stream().map(p->p.getName())
.reduce("All names:", (n1, n2) -> n1 + " " + n2);
System.out.println(allNames); //All names: Bob, Jim, Jill, Bill
现在,结果看起来更好了。在下一小节中演示collect()
操作时,我们将向您展示另一种创建带有前缀的逗号分隔值列表的方法。
现在,让我们看看如何使用第三种形式的reduce()
操作,即具有三个参数的操作,最后一种称为组合器。将组合器添加到前面的reduce()
操作不会改变结果:
String allNames = list.stream().map(p->p.getName())
.reduce("All names:", (n1, n2) -> n1 + " " + n2,
(n1, n2) -> n1 + " " + n2 );
System.out.println(allNames); //All names: Bob, Jim, Jill, Bill
这是因为流不是并行的,并且组合器仅与并行流一起使用。
如果我们使流平行,结果会发生变化:
String allNames = list.parallelStream().map(p->p.getName())
.reduce("All names:", (n1, n2) -> n1 + " " + n2,
(n1, n2) -> n1 + " " + n2 );
System.out.println(allNames);
//All names: Bob All names: Jim All names: Jill All names: Bill
显然,对于并行流,元素序列被分解成子序列,每个子序列被独立地处理;它们的结果由组合器聚合。执行此操作时,组合器将初始值(标识)添加到每个结果中。即使我们移除合并器,并行流处理的结果仍然相同,因为提供了默认的合并器行为:
String allNames = list.parallelStream().map(p->p.getName())
.reduce("All names:", (n1, n2) -> n1 + " " + n2);
System.out.println(allNames);
//All names: Bob All names: Jim All names: Jill All names: Bill
在前面两种形式的reduce()
操作中,累加器使用标识值。在第三种形式中,U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
签名的身份值由组合器使用(注意,U
类型是组合器类型)。
为了消除结果中的重复标识值,我们决定将其从组合器中的第二个参数中删除:
allNames = list.parallelStream().map(p->p.getName())
.reduce("All names:", (n1, n2) -> n1 + " " + n2,
(n1, n2) -> n1 + " " + StringUtils.remove(n2, "All names:"));
System.out.println(allNames); //All names: Bob, Jim, Jill, Bill
正如您所看到的,结果现在看起来好多了。
到目前为止,在我们的示例中,标识不仅扮演初始值的角色,还扮演结果中标识符(标签)的角色。当流的元素是数字时,标识看起来更像是初始值。让我们看一下以下示例:
List<Integer> ints = List.of(1, 2, 3);
int sum = ints.stream().reduce((i1, i2) -> i1 + i2).orElse(0);
System.out.println(sum); //prints: 6
sum = ints.stream().reduce(Integer::sum).orElse(0);
System.out.println(sum); //prints: 6
sum = ints.stream().reduce(10, Integer::sum);
System.out.println(sum); //prints: 16
sum = ints.stream().reduce(10, Integer::sum, Integer::sum);
System.out.println(sum); //prints: 16
前两个流管道完全相同,只是第二个管道使用方法引用而不是 lambda 表达式。第三个和第四个管道也具有相同的功能。它们都使用初始值 10。现在,第一个参数作为初始值比标识更有意义,不是吗?在第四个管道中,我们添加了一个合并器,但没有使用它,因为流不是平行的。
让我们平行进行,看看会发生什么:
List<Integer> ints = List.of(1, 2, 3);
int sum = ints.parallelStream().reduce(10, Integer::sum, Integer::sum);
System.out.println(sum); //prints: 36
结果是 36,因为初始值 10 与每个部分结果相加三次。显然,这条河被分成了三个子序列。但情况并非总是如此,它会随着流的增长和计算机上 CPU 的数量的增加而变化。因此,不能依赖于固定数量的子序列,最好不要将其用于此类情况,并在需要时添加到结果中:
List<Integer> ints = List.of(1, 2, 3);
int sum = ints.parallelStream().reduce(0, Integer::sum, Integer::sum);
System.out.println(sum); //prints: 6
sum = 10 + ints.parallelStream().reduce(0, Integer::sum, Integer::sum);
System.out.println(sum); //prints: 16
collect()
操作的一些用法非常简单,建议初学者使用,而其他情况可能很复杂,即使是经验丰富的程序员也无法使用。结合前面讨论的操作,我们在本节中介绍的collect()
最流行的案例足以满足初学者的所有需求。添加我们将在数字流接口一节中介绍的数字流操作,所涵盖的内容可能很容易成为主流程序员在可预见的未来所需要的全部内容。
正如我们已经提到的,collect 操作非常灵活,允许我们定制流处理。它有两种形式:
R collect(Collector<T, A, R> collector)
:使用提供的Collector
处理T
类型流的元素,并通过A
类型的中间累积生成R
类型的结果R collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)
:使用提供的功能处理T
类型流的元素:Supplier<R>
:创建一个新的结果容器BiConsumer<R, T> accumulator
:向结果容器添加元素的无状态函数BiConsumer<R, R> combiner
:一个无状态函数,合并两个部分结果容器,将第二个结果容器中的元素添加到第一个结果容器中。
让我们看一下collect()
操作的第二种形式。这与reduce()
操作非常相似,我们刚刚演示了三个参数。最大的区别在于,collect()
操作中的第一个参数不是标识或初始值,而是将在函数之间传递并保持处理状态的对象。对于以下示例,我们将使用Person1
类作为容器:
class Person1 {
private String name;
private int age;
public Person1(){}
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() {return this.age; }
public void setAge(int age) { this.age = age;}
@Override
public String toString() {
return "Person{name:" + this.name + ",age:" + age + "}";
}
}
如您所见,容器必须有一个没有参数和 setter 的构造函数,因为它应该能够接收并保留部分结果,即迄今为止年龄最大的人的姓名和年龄。collect()
操作将在处理每个元素时使用此容器,并且在处理最后一个元素后,将包含最年长者的姓名和年龄。以下是您应该熟悉的人员列表:
List<Person> list = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
下面是collect()
操作,应该可以找到列表中最年长的人:
Person1 theOldest = list.stream().collect(Person1::new,
(p1, p2) -> {
if(p1.getAge() < p2.getAge()){
p1.setAge(p2.getAge());
p1.setName(p2.getName());
}
},
(p1, p2) -> { System.out.println("Combiner is called!"); });
我们尝试在操作调用中内联函数,但看起来有点难以阅读,因此下面是相同代码的更好版本:
BiConsumer<Person1, Person> accumulator = (p1, p2) -> {
if(p1.getAge() < p2.getAge()){
p1.setAge(p2.getAge());
p1.setName(p2.getName());
}
};
BiConsumer<Person1, Person1> combiner = (p1, p2) -> {
System.out.println("Combiner is called!"); //prints nothing
};
theOldest = list.stream().collect(Person1::new, accumulator, combiner);
System.out.println(theOldest); //prints: Person{name:Jim,age:33}
对于第一个元素处理,Person1
容器对象只创建一次(在这个意义上,它类似于reduce()
操作的初始值)。然后将其传递给累加器,累加器将其与第一个元素进行比较。容器中的age
字段被初始化为默认值零,因此,容器中第一个元素的年龄和名称被设置为迄今为止最老的人的参数。
当流的第二个元素(Person
对象)被发射时,其age
字段将与容器中当前存储的age
值(Person1
对象)进行比较,依此类推,直到流的所有元素都被处理。结果显示在前面的注释中。
从未调用组合器,因为流不平行。但当我们使其并行时,我们需要实现如下组合器:
BiConsumer<Person1, Person1> combiner = (p1, p2) -> {
System.out.println("Combiner is called!"); //prints 3 times
if(p1.getAge() < p2.getAge()){
p1.setAge(p2.getAge());
p1.setName(p2.getName());
}
};
theOldest = list.parallelStream()
.collect(Person1::new, accumulator, combiner);
System.out.println(theOldest); //prints: Person{name:Jim,age:33}
组合器比较(所有流子序列的)部分结果并得出最终结果。现在我们看到Combiner is called!
消息打印了三次。但是,与reduce()
操作的情况一样,部分结果(流子序列)的数量可能会有所不同。
现在让我们看一下collect()
操作的第一种形式。它需要实现java.util.stream.Collector<T,A,R>
接口的类的对象,其中T
为流类型,A
为容器类型,R
为结果类型。可以使用Collector
接口的of()
方法创建必要的Collector
对象:
static Collector<T,R,R> of(Supplier<R> supplier, BiConsumer<R,T> accumulator, BinaryOperator<R> combiner, Collector.Characteristics... characteristics)
static Collector<T,A,R> of(Supplier<A> supplier, BiConsumer<A,T> accumulator, BinaryOperator<A> combiner, Function<A,R> finisher, Collector.Characteristics... characteristics)
。
必须传递给前面方法的函数与我们已经演示过的函数类似。但我们不打算这样做有两个原因。首先,它有点复杂,超出了本入门课程的范围,其次,在做这件事之前,我们必须先看看java.util.stream.Collectors
类,它提供了许多现成的收集器。正如我们已经提到的,连同本书中讨论的操作和我们将在数字流接口一节中介绍的数字流操作,它们涵盖了主流编程中的绝大多数处理需求,而且很有可能您根本不需要创建自定义收集器。
java.util.stream.Collectors
类提供了 40 多个创建Collector
对象的方法。我们将仅演示最简单和最流行的:
Collector<T,?,List<T>> toList()
:创建一个收集器,将流元素收集到List
对象中。Collector<T,?,Set<T>> toSet()
:创建一个收集器,将流元素收集到Set
对象中。Collector<T,?,Map<K,U>> toMap (Function<T,K> keyMapper, Function<T,U> valueMapper)
:创建一个收集器,将流元素收集到Map
对象中。Collector<T,?,C> toCollection (Supplier<C> collectionFactory)
:创建一个收集器,将流元素收集到收集工厂指定类型的Collection
对象中。Collector<CharSequence,?,String> joining()
:创建一个收集器,将元素连接成String
值。Collector<CharSequence,?,String> joining (CharSequence delimiter)
:创建一个收集器,将元素连接到分隔符分隔的String
值中。Collector<CharSequence,?,String> joining (CharSequence delimiter, CharSequence prefix, CharSequence suffix)
:创建一个收集器,将元素连接到一个分隔符中,分隔符以String
值分隔,并带有提供的前缀和后缀。Collector<T,?,Integer> summingInt(ToIntFunction<T>)
:创建一个收集器,用于计算应用于每个元素的所提供函数生成的结果之和。同样的方法也适用于long
和double
类型。Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<T>)
:创建一个收集器,用于计算应用于每个元素的所提供函数生成的结果的总和、最小值、最大值、计数和平均值。同样的方法也适用于long
和double
类型。Collector<T,?,Map<Boolean,List<T>>> partitioningBy (Predicate<? super T> predicate)
:创建一个收集器,根据提供的Predicate
函数对元素进行分区。Collector<T,?,Map<K,List<T>>> groupingBy(Function<T,U>)
:创建一个收集器,该收集器使用所提供函数生成的键将元素分组为Map
。
下面的演示代码显示了如何使用这些方法创建的收集器。首先,我们演示了toList()
、toSet()
、toMap()
和toCollection()
方法的用法:
List<String> ls = Stream.of("a", "b", "c").collect(Collectors.toList());
System.out.println(ls); //prints: [a, b, c]
Set<String> set = Stream.of("a", "a", "c").collect(Collectors.toSet());
System.out.println(set); //prints: [a, c]
List<Person> persons = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
Map<String, Person> map = persons.stream()
.collect(Collectors.toMap(p->p.getName() + "-" + p.getAge(), p->p));
System.out.println(map); //prints: {Bob-23=Person{name:Bob,age:23},
Bill-27=Person{name:Bill,age:27},
Jill-28=Person{name:Jill,age:28},
Jim-33=Person{name:Jim,age:33}}
Set<Person> personSet = persons.stream()
.collect(Collectors.toCollection(HashSet::new));
System.out.println(personSet); //prints: [Person{name:Bill,age:27},
Person{name:Jim,age:33},
Person{name:Bob,age:23},
Person{name:Jill,age:28}]
joining()
方法允许在带前缀和后缀的分隔列表中连接Character
和String
值:
List<String> list = List.of("a", "b", "c", "d");
String result = list.stream().collect(Collectors.joining());
System.out.println(result); //abcd
result = list.stream().collect(Collectors.joining(", "));
System.out.println(result); //a, b, c, d
result = list.stream()
.collect(Collectors.joining(", ", "The result: ", ""));
System.out.println(result); //The result: a, b, c, d
result = list.stream()
.collect(Collectors.joining(", ", "The result: ", ". The End."));
System.out.println(result); //The result: a, b, c, d. The End.
summingInt()
和summarizingInt()
方法创建收集器,用于计算应用于每个元素的所提供函数产生的int
值的总和和其他统计信息:
List<Person> list = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
int sum = list.stream().collect(Collectors.summingInt(Person::getAge));
System.out.println(sum); //prints: 111
IntSummaryStatistics stats =
list.stream().collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats); //IntSummaryStatistics{count=4, sum=111,
// min=23, average=27.750000, max=33}
System.out.println(stats.getCount()); //4
System.out.println(stats.getSum()); //111
System.out.println(stats.getMin()); //23
System.out.println(stats.getAverage()); //27.750000
System.out.println(stats.getMax()); //33
还有summingLong()
、summarizingLong()
、summingDouble()
和summarizingDouble()
方法。
partitioningBy()
方法创建一个收集器,该收集器根据提供的条件对元素进行分组,并将组(列表)放入一个Map
对象中,以boolean
值作为键:
List<Person> list = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(28, "Jill"),
new Person(27, "Bill"));
Map<Boolean, List<Person>> map =
list.stream().collect(Collectors.partitioningBy(p->p.getAge() > 27));
System.out.println(map);
//{false=[Person{name:Bob,age:23}, Person{name:Bill,age:27}],
// true=[Person{name:Jim,age:33}, Person{name:Jill,age:28}]}
如您所见,使用p.getAge() > 27
标准,我们可以将所有人分为两组,一组低于或等于 27 岁(关键是false
),另一组高于 27 岁(关键是true
)。
最后,groupingBy()
方法允许我们通过一个值对元素进行分组,并将组(列表)放入一个Map
对象中,该值作为键:
List<Person> list = List.of(new Person(23, "Bob"),
new Person(33, "Jim"),
new Person(23, "Jill"),
new Person(33, "Bill"));
Map<Integer, List<Person>> map =
list.stream().collect(Collectors.groupingBy(Person::getAge));
System.out.println(map);
//{33=[Person{name:Jim,age:33}, Person{name:Bill,age:33}],
// 23=[Person{name:Bob,age:23}, Person{name:Jill,age:23}]}
为了能够演示前面的方法,我们将Person
对象的列表更改为 23 岁或 33 岁。结果按年龄分为两组。
还有重载的toMap()
、groupingBy()
和partitioningBy()
方法,以及以下创建相应Collector
对象的方法(通常也是重载的):
counting()
reducing()
filtering()
toConcurrentMap()
collectingAndThen()
maxBy()
和minBy()
mapping()
和flatMapping()
averagingInt()
、averagingLong()
和averagingDouble()
toUnmodifiableList()
、toUnmodifiableMap()
和toUnmodifiableSet()
如果在本书中讨论的操作中找不到所需的操作,请先搜索Collectors
API,然后再构建自己的Collector
对象。
如前所述,IntStream
、LongStream
和DoubleStream
三个数字接口的方法都与Stream
接口中的方法类似,包括Stream.Builder
接口的方法。这意味着我们在本章中讨论的所有内容同样适用于任何数字流接口。这就是为什么在本节中,我们只讨论那些在Stream
接口中不存在的方法:
IntStream
和LongStream
接口中的range(lower,upper)
和rangeClosed(lower,upper)
方法。它们允许我们从指定范围内的值创建流。boxed()
和mapToObj()
中间操作,将数字流转换为Stream
。mapToInt()
、mapToLong()
和mapToDouble()
中间操作,将一种类型的数字流转换为另一种类型的数字流。flatMapToInt()
、flatMapToLong()
和flatMapToDouble()
中间操作,将流转换为数字流。sum()
和average()
终端操作,用于计算数字流元素的总和和平均值。
除了Stream
接口创建流的方法外,IntStream
和LongStream
接口允许我们从指定范围内的值创建流。
range(lower, upper)
方法按顺序生成所有值,从lower
值开始,以upper
之前的值结束:
IntStream.range(1, 3).forEach(System.out::print); //prints: 12
LongStream.range(1, 3).forEach(System.out::print); //prints: 12
rangeClosed(lower, upper)
方法按顺序生成所有值,从lower
值开始,以upper
值结束:
IntStream.rangeClosed(1, 3).forEach(System.out::print); //prints: 123
LongStream.rangeClosed(1, 3).forEach(System.out::print); //prints: 123
除Stream
中间操作外,IntStream
、LongStream
、DoubleStream
接口还具有编号特定的中间操作:boxed()
、mapToObj()
、mapToInt()
、mapToLong()
、mapToDouble()
、flatMapToInt()
、flatMapToLong()
、flatMapToDouble()
。
boxed()
中间操作将原语数字类型的(box)元素转换为相应的包装器类型:
//IntStream.range(1, 3).map(Integer::shortValue) //compile error
// .forEach(System.out::print);
IntStream.range(1, 3).boxed().map(Integer::shortValue)
.forEach(System.out::print); //prints: 12
//LongStream.range(1, 3).map(Long::shortValue) //compile error
// .forEach(System.out::print);
LongStream.range(1, 3).boxed().map(Long::shortValue)
.forEach(System.out::print); //prints: 12
//DoubleStream.of(1).map(Double::shortValue) //compile error
// .forEach(System.out::print);
DoubleStream.of(1).boxed().map(Double::shortValue)
.forEach(System.out::print); //prints: 1
在前面的代码中,我们注释掉了生成编译错误的行,因为range()
方法生成的元素是基元类型。通过添加boxed()
操作,我们将原语值转换为相应的包装类型,然后可以将它们作为引用类型进行处理。
mapToObj()
中间操作执行类似的转换,但它不像boxed()
操作那样专门化,允许使用原语类型的元素生成任何类型的对象:
IntStream.range(1, 3).mapToObj(Integer::valueOf)
.map(Integer::shortValue)
.forEach(System.out::print); //prints: 12
IntStream.range(42, 43).mapToObj(i -> new Person(i, "John"))
.forEach(System.out::print);
//prints: Person{name:John,age:42}
LongStream.range(1, 3).mapToObj(Long::valueOf)
.map(Long::shortValue)
.forEach(System.out::print); //prints: 12
DoubleStream.of(1).mapToObj(Double::valueOf)
.map(Double::shortValue)
.forEach(System.out::print); //prints: 1
在前面的代码中,我们添加了map()
操作,只是为了证明mapToObj()
操作完成了任务,并按照预期创建了一个包装类型对象。此外,通过添加生成Person
对象的流管道,我们已经演示了如何使用mapToObj()
操作创建任何类型的对象。
mapToInt()
、mapToLong()
、mapToDouble()
中间操作允许我们将一种类型的数字流转换为另一种类型的数字流。对于演示代码,我们通过将每个String
值映射到其长度,将String
值列表转换为不同类型的数字流:
list.stream().mapToInt(String::length)
.forEach(System.out::print); //prints: 335
list.stream().mapToLong(String::length)
.forEach(System.out::print); //prints: 335
list.stream().mapToDouble(String::length)
.forEach(d -> System.out.print(d + " ")); //prints: 3.0 3.0 5.0
创建的数字流的元素属于基元类型:
//list.stream().mapToInt(String::length)
// .map(Integer::shortValue) //compile error
// .forEach(System.out::print);
由于我们正在讨论这个主题,如果您想将元素转换为数字包装类型,map()
中间操作是实现这一点的方法(而不是mapToInt()
:
list.stream().map(String::length)
.map(Integer::shortValue)
.forEach(System.out::print); //prints: 335
flatMapToInt()
、flatMapToLong()
、flatMapToDouble()
中间操作产生相应类型的数字流:
List<Integer> list = List.of(1, 2, 3);
list.stream().flatMapToInt(i -> IntStream.rangeClosed(1, i))
.forEach(System.out::print); //prints: 112123
list.stream().flatMapToLong(i -> LongStream.rangeClosed(1, i))
.forEach(System.out::print); //prints: 112123
list.stream().flatMapToDouble(DoubleStream::of)
.forEach(d -> System.out.print(d + " ")); //prints: 1.0 2.0 3.0
如您所见,在前面的代码中,我们在原始流中使用了int
值。但它可以是任何类型的流:
List<String> str = List.of("one", "two", "three");
str.stream().flatMapToInt(s -> IntStream.rangeClosed(1, s.length()))
.forEach(System.out::print); //prints: 12312312345
数字流的附加终端操作非常简单。其中有两个:
sum()
:计算数字流元素的总和average()
:计算数值流元素的平均值
如果需要计算数值流元素值的总和或平均值,则流的唯一要求是它不应是无限的。否则,计算将永远不会完成:
int sum = IntStream.empty().sum();
System.out.println(sum); //prints: 0
sum = IntStream.range(1, 3).sum();
System.out.println(sum); //prints: 3
double av = IntStream.empty().average().orElse(0);
System.out.println(av); //prints: 0.0
av = IntStream.range(1, 3).average().orElse(0);
System.out.println(av); //prints: 1.5
long suml = LongStream.range(1, 3).sum();
System.out.println(suml); //prints: 3
double avl = LongStream.range(1, 3).average().orElse(0);
System.out.println(avl); //prints: 1.5
double sumd = DoubleStream.of(1, 2).sum();
System.out.println(sumd); //prints: 3.0
double avd = DoubleStream.of(1, 2).average().orElse(0);
System.out.println(avd); //prints: 1.5
如您所见,在空流上使用这些操作不是问题。
我们已经看到,如果没有为处理并行流而编写和测试代码,那么从顺序流更改为并行流可能会导致错误的结果。以下是与并行流相关的更多注意事项。
存在无状态操作,例如filter()
、map()
和flatMap()
,它们在从一个流元素到下一个流元素的处理过程中不保留数据(不维护状态)。并且存在状态操作,例如distinct()
、limit()
、sorted()
、reduce()
和collect()
,它们可以将状态从先前处理的元素传递到下一个元素的处理。
从顺序流切换到并行流时,无状态操作通常不会造成问题。每个元素都是独立处理的,流可以被分解成任意数量的子流进行独立处理。
对于有状态操作,情况就不同了。首先,将它们用于无限流可能永远无法完成处理。此外,在讨论reduce()
和collect()
有状态操作时,我们已经演示了在没有考虑并行处理的情况下设置初始值(或标识)时,切换到并行流如何产生不同的结果。
还有性能方面的考虑。有状态操作通常需要使用缓冲在几个过程中处理所有流元素。对于大数据流,它可能会占用 JVM 资源,并降低应用程序的速度(如果不是完全关闭的话)。
这就是为什么程序员不应该轻率地从顺序流切换到并行流的原因。如果涉及有状态操作,则必须设计和测试代码,以便能够在没有负面影响的情况下执行并行流处理。
正如我们在上一节中指出的,并行处理可能会也可能不会产生更好的性能。在决定使用之前,必须测试每个用例。并行性可以产生更好的性能,但必须对代码进行设计并可能进行优化才能做到这一点。每个假设都必须在尽可能接近生产的环境中进行测试。
但是,在决定顺序处理和并行处理时,可以考虑以下几点:
- 小数据流通常按顺序处理得更快(那么,对于您的环境来说,什么是“小”应该通过测试和测量性能来确定)
- 如果有状态的操作不能被无状态的操作所取代,请仔细设计并行处理的代码,或者干脆完全避免它
- 考虑并行处理的过程需要大量的计算,但考虑把部分结果一起为最终结果
使用流将以下列表中的所有值相乘:
List<Integer> list = List.of(2, 3, 4);
int r = list.stream().reduce(1, (x, y) -> x * y);
System.out.println(r); //prints: 24
本章介绍了数据流处理的强大概念,并提供了许多函数式编程使用的示例。它解释了什么是流,如何处理它们,以及如何构建处理管道。它还演示了如何并行组织流处理以及一些可能的陷阱。
在下一章中,我们将讨论反应式系统、它们的优点和可能的实现。您将使用这些主要代码和非反应式编程原理来演示哪些是基于反应式编程的非反应式系统。*