流对于表达数据处理查询是非常强大而有用的。到目前为止,已经能够使用stream方法从集合生成流了。此外,还介绍了如何根据数值范围创建数值流。下面会介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流。
可以使用静态方法Stream.of
,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用Stream.of
创建了一个字符串流。然后,可以将字符串转换为大写,再一个个打印出来:
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
可以使用empty
得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
可以使用静态方法Arrays.stream
从数组创建一个流。它接受一个数组作为参数。例如,可以将一个原始类型int
的数组转换成一个IntStream
,如下所示:
int[] numbers ={2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();//总和是41
Java
中用于处理文件等I/O
操作的NIO API
(非阻塞I/O
)已更新,以便利用Stream API
。java.nio.file.Files
中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines
,它会返回一个由指定文件中的各行构成的字符串流。可以用这个方法看看一个文件中有多少各不相同的词:
long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset ())) {//流会自动关闭
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split("")))//生成单词流
.distinct()//删除重复项
.count();//数一数有多少各不相同的单词
}
catch(IOException e) {
//如果打开文件时出现异常则加以处理
}
可以使用Files.lines
得到一个流,其中的每个元素都是给定文件中的一行。然后,可以对line
调用split
方法将行拆分成单词。应该注意的是,该如何使用flatMap
产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinct
和count
方法链接起来,数数流中有多少各不相同的单词。
Stream API
提供了两个静态方法来从函数生成流:Stream.iterate
和stream.generate
。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate
产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用limit(n)
来对这种流加以限制,以避免打印无穷多个值。
一个iterate的简单例子:
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate
方法接受一个初始值(在这里是0
),还有一个依次应用在每个产生的新值上的Lambda
(Unaryoperator
类型)。这里,使用Lambda n -> n +2
,返回的是前一个元素加上2
。因此,iterate
方法生成了一个所有正偶数的流:流的第一个元素是初始值0
。然后加上2
来生成新的值2
,再加上2
来得到新的值4
,以此类推。这种iterate
操作基本上是顺序的,因为结果取决于前一次应用。此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。这个流是无界的。这是流和集合之间的一个关键区别。使用limit
方法来显式限制流的大小。这里只选择了前10
个偶数。然后可以调用forEach
终端操作来消费流,并分别打印每个元素。
一般来说,在需要依次生成一系列值的时候应该使用iterate
,比如一系列日期:1月31日,2月1日,依此类推。
与iterate
方法类似,generate
方法也可按需生成一个无限流。但generate
不是依次对每个新生成的值应用函数的。它接受一个supplier
类型的Lambda
提供新的值。先来看一个简单的用法:
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
这段代码将生成一个流,其中有五个0
到1
之间的随机双精度数。例如,运行一次得到了下面的结果:
0.9410810294106129
0.6586270755634592
0.9592859117266873
0.13743396659487006
0.3942776037651241
Math.Random
静态方法被用作新值生成器。同样,可以用limit
方法显式限制流的大小,否则流将会无限长。
使用的供应源(指向Math.random
的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用。
在这个例子中会使用IntStream
说明避免装箱操作的代码。IntStream
的generate
方法会接受一个IntSupplier
,而不是Supplier
。例如,可以这样来生成一个全是1
的无限流:
IntStream ones = IntStream.generate(() -> 1);
Lambda
允许创建函数式接口的实例,只要直接内联提供方法的实现就可以。也可以像下面这样,通过实现IntSupplier
接口中定义的getAsInt
方法显式传递一个对象:
IntStream twos = IntStream.generate(new IntSupplier() {
public int getAsInt() {
return 2;
}
});
generate
方法将使用给定的供应源,并反复调用getAsInt
方法,而这个方法总是返回2
。但这里使用的匿名类和Lambda
的区别在于,匿名类可以通过字段定义状态,而状态又可以用getAsInt
方法来修改。这是一个副作用的例子。迄今见过的所有Lambda
都是没有副作用的;它们没有改变任何状态。
回到斐波纳契数列的任务上,现在需要做的是建立一个IntSupplier
,它要把前一项的值保存在状态中,以便getAsInt
用它来计算下一项。此外,在下一次调用它的时候,还要更新IntSupplier
的状态。下面的代码就是如何创建一个在调用时返回下一个斐波纳契项的IntSupplier
:
IntSupplier fib = new IntSupplier() {
private int previous = 0;
private int current = 1;
public int getAsInt () {
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
前面的代码创建了一个IntSupplier
的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。getAsInt
在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,使用iterate
的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。
现在可以更高效地处理集合了。流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化。以下是关键概念。
Streams API
可以表达复杂的数据处理查询。filter
、distinct
、skip
和limit
对流做筛选和切片。map
和flatMap
提取或转换流中的元素。findFirst
和findAny
方法查找流中的元素。allMatch
、noneMatch
和anyMatch
方法让流匹配给定的谓词。reduce
方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。filter
和map
等操作是无状态的,它们并不存储任何状态。reduce
等操作要存储状态才能计算出一个值。sorted
和distinct
等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。IntStream
、DoubleStream
和LongStream
。它们的操作也有相应的特化。iterate
与generate
等特定方法创建。