《Java 8 in Action》【05】----使用流(二)

1.数值流

  前面提到通过reduce方法可以计算流中元素的总和,例如计算菜单的总热量。

int calories = menu.stream()
	.map(Dish::getCalories)
	.reduce(0, Integer::sum);

  这段代码的问题在于暗含了一个装箱的成本,每个Integer必须拆箱成一个原始类型,再进行求和。对于这情况,Stream API提供了一个原始类型流特化,专门支持处理数值流的方法。

1.1 原始类型流特化

  Java8中提供了三个原始类型特化流接口来解决上面提到问题:IntStreamDoubleStreamLongStream,分别将流中的元素特化为intlongdouble,从而避免了暗含的装箱成本。每个接口都提供了进行常用数值归约的方法,比如对数值流求和的 sum方法,找到最大元素的max方法。此外还有在必要时再把它们转换回对象流的boxed方法。特化流存在的原因并不在于流的复杂性,而是装箱造成的复杂性,类似于intInteger之间的效率差异。

————映射到数值流: mapToIntmapToDoublemapToLong
  将流转换为特化流版本常用方法是mapToIntmapToDoublemapToLong。这些方法将会返回一个特化流,而非Stream<T>。例如使用mapToInt对菜单总热量求和:

int calories = menu.stream()//返回一个Stream<Dish>
	.mapToInt(Dish::getCalories)//返回一个IntStream
	.sum();

mapToInt会从每道菜中提取热量(用Integer表示),并返回一个IntStream(而不是一个Stream<Integer>)。然后调用IntStream接口定义sum方法对热量求和。值得注意的是,如果流是空的,sum默认返回0。IntStream还支持其他方法,如maxminaverage等。

————转换回对象流: 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

————默认值 OptionalIntOptionalDoubleOptionalLong
  上面求和的方法,当流中不存在元素时,返回的是默认值0。但用IntStream计算最大值时,如果返回0,就会产生一个问题,无法区分流中是没有元素,还是流中最大值为0的情况。这时候可以用Optional类,它是一个可以表示值存在或者不存在的容器,对于三种原始特化流,Optional也分别有一个对应的原始类特化版本:OptionalIntOptionalDoubleOptionalLong。例如,要查找IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt。如果没有最大值情况下,可以显式定义一个默认值,此处不细说Optional类。

OptionalInt maxCalories = menu.stream()
	.mapToInt(Dish::getCalories)
	.max();
//如果没有最大值的话,显式提供一个默认最大值
int max = maxCalories.orElse(1);
1.2 数值范围

  处理数字时,比较常用的是数值范围,例如要生成1到100之间的数字。Java8的IntStreamLongStream两个类中静态方法rangerangeClosed可以用来生成这种数值范围。这两个方法中的第一个参数接受起始值,第二个参数接受结束值。其中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表示斜边,ab为表示两条直角边,这个定理称为勾股定理。如何在指定数值范围内获取几组满足勾股定理的勾股数呢?可以参考以下步骤:
【步骤一】:勾股数有三个,因此需要定义一个三元数,可以用具有三个元素的int数组来实现,使用数组索引访问数组中每个元素。
【步骤二】:假如给定两个数字ab,可以根据a*a +b*b的平方根即c是不是整数来判定能不能形成一组勾股数。在Java中可以使用expr % 1 == 0表示expr为一个整数。假设现在已经有a值,而流Stream提供了b值,那么可以通过filter方法来进行筛选:

filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)

【步骤三】:如果说经过上面筛选,ab能够组成勾股数,这个时候就需要创建一个三元组,可以使用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)});

【步骤四】:前面提到bStream提供的,因此使用数值流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,这是由于原始类型流IntStreammap只能将流中元素映射为一个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.rangeCloseda提供来源。因此最终解决方式是:

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.iterateStream.generate创建无限流
  Stream API提供了两个静态方法来从函数生成流:Stream.iterateStream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterategenerate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去。一般来说,应该使用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.总结
  1. 流有三种基本的原始类型特化: IntStreamDoubleStreamLongStream 。它们的操作也有相应的特化。
  2. 流不仅可以从集合创建,也可从值、数组、文件以及iterategenerate等特定方法创建。
  3. 无限流是没有固定大小的流。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值