1.数值流
前面提到通过reduce
方法可以计算流中元素的总和,例如计算菜单的总热量。
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
这段代码的问题在于暗含了一个装箱的成本,每个Integer
必须拆箱成一个原始类型,再进行求和。对于这情况,Stream API提供了一个原始类型流特化,专门支持处理数值流的方法。
1.1 原始类型流特化
Java8中提供了三个原始类型特化流接口来解决上面提到问题:IntStream
、DoubleStream
和LongStream
,分别将流中的元素特化为int
、long
和double
,从而避免了暗含的装箱成本。每个接口都提供了进行常用数值归约的方法,比如对数值流求和的 sum
方法,找到最大元素的max
方法。此外还有在必要时再把它们转换回对象流的boxed
方法。特化流存在的原因并不在于流的复杂性,而是装箱造成的复杂性,类似于int
和Integer
之间的效率差异。
————映射到数值流: mapToInt
、 mapToDouble
和 mapToLong
将流转换为特化流版本常用方法是mapToInt
、mapToDouble
和mapToLong
。这些方法将会返回一个特化流,而非Stream<T>
。例如使用mapToInt
对菜单总热量求和:
int calories = menu.stream()//返回一个Stream<Dish>
.mapToInt(Dish::getCalories)//返回一个IntStream
.sum();
mapToInt
会从每道菜中提取热量(用Integer表示),并返回一个IntStream
(而不是一个Stream<Integer>
)。然后调用IntStream
接口定义sum
方法对热量求和。值得注意的是,如果流是空的,sum
默认返回0。IntStream
还支持其他方法,如max
、min
、average
等。
————转换回对象流: boxed
有了数值流,如果想转回非特化流。例如IntStream
上的操作只能产生原始整数,它的map(IntUnaryOperator mapper)
方法接收的一个int->int
的Lambda表达式。但如果需要返回另外类型的值,这时候就需要将原始流转回一般流Stream
(每个int
将会装箱成一个Integer
),可以使用boxed
方法。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);//将 Stream转换为数值流
Stream<Integer> stream = intStream.boxed(); //将数值流转换为Stream
————默认值 OptionalInt
、 OptionalDouble
和 OptionalLong
上面求和的方法,当流中不存在元素时,返回的是默认值0。但用IntStream
计算最大值时,如果返回0,就会产生一个问题,无法区分流中是没有元素,还是流中最大值为0的情况。这时候可以用Optional
类,它是一个可以表示值存在或者不存在的容器,对于三种原始特化流,Optional
也分别有一个对应的原始类特化版本:OptionalInt
、OptionalDouble
和OptionalLong
。例如,要查找IntStream
中的最大元素,可以调用max
方法,它会返回一个OptionalInt
。如果没有最大值情况下,可以显式定义一个默认值,此处不细说Optional
类。
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
//如果没有最大值的话,显式提供一个默认最大值
int max = maxCalories.orElse(1);
1.2 数值范围
处理数字时,比较常用的是数值范围,例如要生成1到100之间的数字。Java8的IntStream
和LongStream
两个类中静态方法range
和 rangeClosed
可以用来生成这种数值范围。这两个方法中的第一个参数接受起始值,第二个参数接受结束值。其中range
是不包含结束值的,而rangeClosed
则包含结束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100) //1-100数字,包含100.
.filter(n -> n % 2 == 0); //求偶数
System.out.println(evenNumbers.count());//偶数个数
本例使用IntStream
中的rangClosed
方法生成1到100(包含)的所有数字,并通过filter
筛选出所有偶数,而后调用终端操作count
方法得到偶数的个数,这里结果是50。如果使用range
,打印结果将会是49,因为不包含100。
1.3 数值流应用:勾股数
数学中直角三角形的三条边满足a*a+b*b=c*c
,其中c
表示斜边,a
、b
为表示两条直角边,这个定理称为勾股定理。如何在指定数值范围内获取几组满足勾股定理的勾股数呢?可以参考以下步骤:
【步骤一】:勾股数有三个,因此需要定义一个三元数,可以用具有三个元素的int
数组来实现,使用数组索引访问数组中每个元素。
【步骤二】:假如给定两个数字a
和b
,可以根据a*a +b*b
的平方根即c
是不是整数来判定能不能形成一组勾股数。在Java中可以使用expr % 1 == 0
表示expr为一个整数。假设现在已经有a
值,而流Stream提供了b
值,那么可以通过filter
方法来进行筛选:
filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
【步骤三】:如果说经过上面筛选,a
和b
能够组成勾股数,这个时候就需要创建一个三元组,可以使用map
操作,将每个元素转换成勾股数组:
stream.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
【步骤四】:前面提到b
是Stream
提供的,因此使用数值流IntStream.rangeClosed
在给定范围生成一个数值流,用它给b
提供值,此处为1到100。
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.boxed()
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
值得注意的是在filter
方法之后调用了boxed
,这是由于原始类型流IntStream
的map
只能将流中元素映射为一个int类型,而这里需要返回一个int
数组,所以使用boxed
将特化流转回了非特化流。实际上也可以调用mapToObj
来返回一个对象值流。
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
【步骤五】:前面有一个关键假设:给出了a
值。现在,只要已知a
的值,就有了一个可以生成勾股数的流。跟b
值一样同样,可以采用数值流IntStream.rangeClosed
为a
提供来源。因此最终解决方式是:
Stream<int[]> pythagoreanTriples =
IntStream.rangeClosed(1, 100)
.boxed()
.flatMap(a ->IntStream.rangeClosed(a, 100)
.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
.mapToObj(b ->new int[]{a, b, (int)Math.sqrt(a * a + b * b)}));
这里首先创建一个从1到100的数值范围生成a
的值。对每个给定的a
值,创建一个三元流。要是把a
值映射到三元流的话,就会得到一个流构成的流。而使用flatMap
方法可以做映射的同时,将所有生成的三元数流扁平化为一个流。这样就得到一个三元数流。还要注意,这里将b
的返回改成了a
到100,因为没必要从1开始,不然会造成重复三元流,如(3,4,5)和(4,3,5)。
实际上这段代码可以改进,因为涉及两次求平方根的操作,可以先生成所有三元数,然后再筛选符合条件的数组:
Stream<double[]> pythagoreanTriples2 = IntStream.rangeClosed(1, 100)
.boxed()
.flatMap(a ->IntStream.rangeClosed(a, 100).mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)})
.filter(t -> t[2] % 1 == 0));//即c必须是整数
2.构建流
目前为止,通过stream
方法从集合生成流,还有根据数值范围创建一个数值流。其实创建流的方法还有很多,本节将介绍如何从值序列、数组、文件来创建流,甚至从生成函数来创建无限流。
————由值创建流:Stream.of()
/Stream.empty()
可以使用静态方法Stream.of
,来显式创建一个流。它接受任意数量的参数。例如下面创建一个字符串流,并将字符串转换成大写,再进行打印:
Stream.of("Java 8 ", "Lambdas ", "In ", "Action") //Stream<String>
.map(String::toUpperCase)
.forEach(System.out::println);
也可以使用Stream.empty()
得到一个空流:
Stream<String> emptyStream = Stream.empty();
————由数组创建流:Arrays.stream()
用静态方法Arrays.stream
从数组创建一个流。它接受一个数组作为参数。例如可以将一个原始类型int
的数组转换成一个 IntStream
,然后用它的sum
方法求和。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
————由文件生成流:Files.lines
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) {
}
————由函数生成流:Stream.iterate
和 Stream.generate
创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate
和Stream.generate
。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate
和generate
产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用limit(n)
来对这种流加以限制,以避免打印无穷多个值。
1.Stream.iterate(final T seed, final UnaryOperator<T> f)
iterate
方法接收一个初始值,以及一个依次应用在每个产生的数值上的Lambda。例如:
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
此处Lambda n -> n + 2
,返回的是前一个元素加上2。所以iterate
方法生成了一个所有正偶数流,流中第一个元素是初始值0,然后再加上2生成新的值2,再加上2得到新的值4,以此类推。这种iterate
操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流,这个流没有结尾,因为值是按需计算的,可以永远计算下去。可以使用limit来显式限制流的大小。
2.generate(Supplier<T> s)
与 iterate
方法类似,generate
方法也可生成一个无限流。但generate
不是依次对每个新生成的值应用函数的。它接受一个 Supplier<T>
类型的Lambda来提供新的值。例如生成一个随机双精度数的流,并取前面5个。
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
3.总结
- 流有三种基本的原始类型特化:
IntStream
、DoubleStream
和LongStream
。它们的操作也有相应的特化。 - 流不仅可以从集合创建,也可从值、数组、文件以及
iterate
与generate
等特定方法创建。 - 无限流是没有固定大小的流。