1.数值流
我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:
int calories =
menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始数据类型,再进行求和。要是可以直接像下面调用sum方法就好了:
int calories =
menu.stream()
.map(Dish::getCalories)
.sum();
但是这是不可能的。问题在于map方法会生成一个Stream<T>。虽然流中的元素是Integer类型,但是Streams接口没有定义sum方法。为什么没有呢?比方说,你只有一个向menu那样的Stream<Dish>,把各种菜加起来是没有任何意义。但不要担心,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。
(1).原始类型特化
Java8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化称为int、long和double。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把他们转换回对象的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性--即类似int和Integer之间的效率差异。
A.映射到数值流
将流转换为特换版本的常用是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是他们返回的是一个特化流,而不是Stream<T>。例如,你可以像下面这样用mapToInt对menu中的卡路里总和:
int calories =
menu.stream() //返回一个Stream<Dish>
.mapToInt(Dish::getCalories) //返回一个IntStream
.sum();
这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并且返回一个IntStream(而不是Stream<Integer>)。然后你就可以调用IntStream接口定义的sum方法,对卡路里求和了!请注意,如果流时空的,sum默认返回0.IntStream还支持其他的方便方法,如max、min。average等。
B.转换回对象流
同样,一旦有了数值流,你可能会想把转换回非特化流。例如IntStream上的操作只能产生原始整数:Instream的map操作接收的Lambda必须接收int并且返回int(一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish值。为此,你需要访问Stream接口中定义的那些更加广义的操作。要把原始流转换为一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);// 将Stream转换为数值流
Stream<Integer> boxed = intStream.boxed(); //将数值流转换为Stream
C.默认值OptionalInt
求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或者不存在的容器。Optional可以用Integer、String等参数类型来参数化。对于三种原始流特化,也分别有一个Optional原始数据类型特化版本:OptionalInt、OptionalDouble和OptionalLong。
例如:要找到IntStream中的最大元素,可以调用方法,它会返回一个OptionalInt:
OptionalInt maxCalories =
menu.stream()
.mapToInt(Dish::getCalories)
.max();
现在,如果没有最大值的话,你就可以显示处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(0); //如果没有最大值的话,显示提供一个默认最大值
(2).数值范围
和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1到100之间的所有数字。Java8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接收起始值,第二个参数接收接收结束值。但是rang是不包含结束值的,而rangeClosed则包含结束值。让我们来看一个例子:
IntStream eventNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2== 0); //一个从1到100的偶数流
System.out.println(eventNumbers.count()); //从1到100一共有50个偶数
这里我们用了rangeClosed方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接filter方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count。因为count是一个终端操作,所以处理流,并且返回结果50,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用IntStream.rang(1, 100),则结果将会是49个偶数,因为range是不包含结束值的。
2.构建流
到现在,我们已经知道流对于表达数据处理查询是非常的强大而且有用的。到目前为止,你已经可以能够使用stream方法将集合变成流了。此外,我们还介绍了如何根据竖直范围创建数值流。但是创建流的方法还有好多!
(1).由值创建流
你可以使用静态方法Stream.of,通过显示地创建一个流。它可以接收任意数量的参数。例如,一下代码直接使用Stream.of创建了一个字符流。然后,你可以讲字符串转换为大小,再一个个打印出来:
Stream<String> stream = Stream.of("Java 8", "Android", "Lambda");
stream.map(String::toUpperCase).forEach(System.out::println);
你可以使用empty得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
(2).由数组创建流
你可以使用静态方法Arrays.stream从数组创建一个流。它接收一个数组作为参数。例如,你可以使用讲一个原始类型int的数组转换成一个IntStream,如下所示:
int[] numbers = { 2, 3, 5, 7, 11, 13 };
int sum = Arrays.stream(numbers).sum();
(3).由文件生成流
Java中用于处理文件等I/O操作的NIO API(非阻塞I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是File.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今学到的内容,你可以用这个方法看看这个文件有多少个不相同的词:
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("D://a.txt"), Charset.defaultCharset())) { //流会自动关闭
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) //生成单词流
.distinct() //删除重复项
.count(); //数一数有多少各不相同的单词
} catch (IOException e) {
//如果打开文件是出现异常则加以处理
}
你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用splie方法将行拆分成一个单词。应该注意的是,你该如何利用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流对象。最后,把distinct和count方法链接起来,数一数流有多少各不相同的单词。
(4).由函数生成流:创建无限流
Stream APi 提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像固定集合创建的流那样有固定大小的流。有iterate和generate产生的流会用给定的函数按需要创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n) 来对这种流加以限制,以避免打印无穷个个值。
A.迭代
我们先来看一个iterate的简单例子,然后解释:
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate方法接收一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<T>类型)。这里,我们使用Lambda表达式 n -> n+2,返回的是前一个元素加上2.因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,,以此类推。这种iterate操作基本上是顺序的,因为结果取决于前一次的结果。请注意,此操作将生成一个无限流--这个流是无界的。正如我们前面所讨论的,这是流和集合之间的关键区别。我们使用limit方法来显示限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并且分别打印每个元素。
一般来说,在需要依次生成一系列的值的时候应该使用iterate方法,比如一系列日期:1月31日,2月1日,以此类推。
B.生成
iterate方法类似,generate方法也可以让你按需生成一个无限流。但是generate不是依次对每个新生成的值应用函数的。它接收一个Supplier<T>类型的Lambda提供新的值。我们先来看一个简单的用法:
Stream.generate(() -> (int)(Math.random() * 63))
.distinct()
.limit(63)
.sorted(Integer::compareTo)
.forEach(System.out::println);
这段代码将生流,其中63个0到63之间的随机数。
Math.random静态方法被用作新值生成器。同样limit方法显示限制流的大小,否则流会无限长。
你可能会想到,generate方法还有什么用途。我们使用的供应源(指向Math.random的Lambda表达式)是无状态的:它不会在任何地方记录新值,以备以后计算使用。但是供应源不一定是无状态的。你可以创建存储状态的供应源,你可以修改状态,并在为流生成下一个值时使用。举个例子,我们将如何利用generate创建斐波那契数列,这样就可以利用iterate方法的办法比较一下。但是很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用。
我们在这个例子中会使用IntSrream说明避免装箱操作的代码。IntStream的generate方法会接收一个IntSupplier,而不是Supplier<T>。例如,可以这样来生成一个全是1的无限流:
IntStream ones = IntStream.generate(() -> 1);
之前,我们已经知道,Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。你也可以像下面这样,通过实现IntSupplier接扣中定义的getAsInt方法显示传递一个对象(虽然这看起来是无缘无故的圈子,也请你耐心的看):
IntStream tows = IntStream.generate(new IntSupplier() {
@Override
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;
@Override
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方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元祖。你在之后了解到,你应该采用始终不变的方法,以便并行处理流,并且保持结果正确。请注意,因为你处理的是一个无限流,所以必须使用limit方法来限制它的大小;否恩泽,终端操作(这里是forEach)将永远计算下去。同样,你不能对无限流做排序或者归约,因为所有的元素都需要处理,而且这永远也完成不了的!