• Java 8实战(八)- 数值流与构建流


    一、数值流

    我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:

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

    这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。要是可以直接像下面这样调用sum方法,岂不是更好?

    int calories = menu.stream()
    					.map(Dish::getCalories)
    					.sum();
    
    • 1
    • 2
    • 3

    但这是不可能的。问题在于map方法会生成一个Stream< T>。虽然流中的元素是Integer类型,但Streams接口没有定义sum方法。为什么没有呢?比方说,你只有一个像menu那样的Stream< Dish>,把各种菜加起来是没有任何意义的。但不要担心,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。

    1. 原始类型流特化

    Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

    (1)映射到数值流

    将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream< T>。例如,你可以像下面这样用mapToInt对menu中的卡路里求和:

    int calories = menu.stream()
    					.mapToInt(Dish::getCalories)
    					.sum();
    
    • 1
    • 2
    • 3

    这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream(而不是一个Stream< Integer>)。然后你就可以调用IntStream接口中定义的sum方法,对卡路里求和了!请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。

    (2)转换回对象流

    同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream上的操作只能产生原始整数: IntStream 的map 操作接受的Lambda 必须接受int 并返回int ( 一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:

    // 将Stream 转换为数值流
    IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
    // 将数值流转换为Stream
    Stream<Integer> stream = intStream.boxed();
    
    • 1
    • 2
    • 3
    • 4

    在需要将数值范围装箱成为一个一般流时,boxed尤其有用。

    (3)默认值OptionalInt

    求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。Optional可以用Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong

    例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:

    OptionalInt maxCalories = menu.stream()
    								.mapToInt(Dish::getCalories)
    								.max();
    
    • 1
    • 2
    • 3

    现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:

    // 如果没有最大值的话,显式提供一个默认最大值
    int max = maxCalories.orElse(1);
    
    • 1
    • 2

    2. 数值范围

    假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。让我们来看一个例子:

    // 表示范围[1, 100]
    // 一个从1到100的偶数流
    IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
    // 从1 到100 有50个偶数
    System.out.println(evenNumbers.count());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里我们用了rangeClosed方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接filter方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count。因为count是一个终端操作,所以它会处理流,并返回结果50,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用IntStream.range(1, 100),则结果将会是49个偶数,因为range是不包含结束值的。

    3. 数值流应用:勾股数

    1. 勾股数
    古希腊数学家毕达哥拉斯发现了某些三元数(a, b, c)满足公式a * a + b * b =c * c,其中a、b、c都是整数。例如,(3, 4, 5)就是一组有效的勾股数,因为3 * 3 + 4 * 4 = 5 * 5或9 + 16 = 25。这样的三元数有无限组。例如,(5, 12, 13)、(6, 8, 10)和(7, 24, 25)都是有效的勾股数

    2. 表示三元数
    第一步是定义一个三元数。虽然更恰当的做法是定义一个新的类来表示三元数,但这里你可以使用具有三个元素的int数组,比如new int[]{3, 4, 5},来表示勾股数(3, 4, 5)。现在你就可以用数组索引访问每个元素了。

    3. 筛选成立的组合
    假定有人为你提供了三元数中的前两个数字:a和b。怎么知道它是否能形成一组勾股数呢?你需要测试a * a + b * b的平方根是不是整数,也就是说它没有小数部分——在Java里可以使用expr % 1表示。如果它不是整数,那就是说c不是整数。你可以用filter操作表达这个要求(你稍后会了解到如何将其连接起来成为有效代码):

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

    假设周围的代码给a提供了一个值,并且stream提供了b可能出现的值,filter将只选出那些可以与a组成勾股数的b。你可能在想Math.sqrt(a * a + b * b) % 1 == 0这一行是怎么回事。简单来说,这是一种测试Math.sqrt(a * a + b * b)返回的结果是不是整数的方法。如果平方根的结果带了小数,如9.1,这个条件就不成立(9.0是可以的)。

    4. 生成三元组
    在筛选之后,你知道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)});
    
    • 1

    5. 生成b值
    现在你需要生成b的值。前面已经看到,Stream.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)});
    
    • 1
    • 2
    • 3
    • 4

    请注意,你在filter之后调用boxed,从rangeClosed返回的IntStream生成一个Stream< Integer>。这是因为你的map会为流中的每个元素返回一个int数组。而IntStream中的map方法只能为流中的每个元素返回另一个int,这可不是你想要的!你可以用IntStream的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)});
    
    • 1
    • 2
    • 3

    6. 生成值
    这里有一个关键的假设:给出了a的值。 现在,只要已知a的值,你就有了一个可以生成勾股数的流。如何解决这个问题呢?就像b一样,你需要为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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    好的,flatMap又是怎么回事呢?首先,创建一个从1到100的数值范围来生成a的值。对每个给定的a值,创建一个三元数流。要是把a的值映射到三元数流的话,就会得到一个由流构成的流。flatMap方法在做映射的同时,还会把所有生成的三元数流扁平化成一个流。这样你就得到了一个三元数流。还要注意,我们把b的范围改成了a到100。没有必要再从1开始了,否则就会造成重复的三元数,例如(3,4,5)和(4,3,5)。

    7. 运行代码
    现在你可以运行解决方案,并且可以利用我们前面看到的limit命令,明确限定从生成的流中要返回多少组勾股数了:

    pythagoreanTriples.limit(5).forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
    
    • 1

    这会打印:
    3, 4, 5
    5, 12, 13
    6, 8, 10
    7, 24, 25
    8, 15, 17

    8. 你还能做得更好吗?
    目前的解决办法并不是最优的,因为你要求两次平方根。让代码更为紧凑的一种可能的方法是,先生成所有的三元数(aa, bb, aa+bb),然后再筛选符合条件的:

    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)
    		);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    二、构建流

    1. 由值创建流

    你可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用Stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再一个个打印出来:

    Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
    stream.map(String::toUpperCase).forEach(System.out::println);
    
    • 1
    • 2

    你可以使用empty得到一个空流,如下所示:

    Stream<String> emptyStream = Stream.empty();
    
    • 1

    2. 由数组创建流

    你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如,你可以将一个原始类型int的数组转换成一个IntStream,如下所示:

    int[] numbers = {2, 3, 5, 7, 11, 13};
    int sum = Arrays.stream(numbers).sum();
    
    • 1
    • 2

    3. 由文件生成流

    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){
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用split方法将行拆分成单词。应该注意的是,你该如何使用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinct和count方法链接起来,数数流中有多少各不相同的单词。

    4. 由函数生成流:创建无限流

    Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

    1. 迭代
    我们先来看一个iterate的简单例子,然后再解释:

    Stream.iterate(0, n -> n + 2)
    		.limit(10)
    		.forEach(System.out::println);
    
    • 1
    • 2
    • 3

    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日,依此类推。

    测验:斐波纳契元组序列
    斐波纳契数列是著名的经典编程练习。下面这个数列就是斐波纳契数列的一部分:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…数列中开始的两个数字是0和1,后续的每个数字都是前两个数字之和。
    斐波纳契元组序列与此类似,是数列中数字和其后续数字组成的元组构成的序列:(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21) …
    你的任务是用iterate方法生成斐波纳契元组序列中的前20个元素。
    第一个问题是,iterate方法要接受一个UnaryOperator< t>作为参数,而你需要一个像(0,1)这样的元组流。你还是可以(这次又是比较草率地)使用一个数组的两个元素来代表元组。例如,new int[]{0,1}就代表了斐波纳契序列(0, 1)中的第一个元素。这就是iterate方法的初始值:

    Stream.iterate(new int[]{0, 1}, ???)
    		.limit(20)
    		.forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
    
    • 1
    • 2
    • 3

    在这个测验中,你需要搞清楚???代表的代码是什么。请记住,iterate会按顺序应用给定的Lambda。答案:

    Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0]+t[1]})
    		.limit(20)
    		.forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
    
    • 1
    • 2
    • 3

    它是如何工作的呢?iterate需要一个Lambda来确定后续的元素。对于元组(3, 5),其后续元素是(5, 3+5) = (5, 8)。下一个是(8, 5+8)。看到这个模式了吗?给定一个元组,其后续的元素是(t[1], t[0] + t[1])。这可以用这个Lambda来计算:t->new int[]{t[1], t[0]+t[1]}。运行这段代码,你就得到了序列(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21)…请注意,如果你只想打印正常的斐波纳契数列,可以使用map提取每个元组中的第一个元素:

    Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1],t[0] + t[1]})
    		.limit(10)
    		.map(t -> t[0])
    		.forEach(System.out::println);
    
    • 1
    • 2
    • 3
    • 4

    这段代码将生成斐波纳契数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34…

    2. 生成
    与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier< T>类型的Lambda提供新的值。我们先来看一个简单的用法:

    Stream.generate(Math::random)
    		.limit(5)
    		.forEach(System.out::println);
    
    • 1
    • 2
    • 3

    这段代码将生成一个流,其中有五个0到1之间的随机双精度数。例如,运行一次得到了下面的结果:
    0.9410810294106129
    0.6586270755634592
    0.9592859117266873
    0.13743396659487006
    0.3942776037651241

    Math.Random静态方法被用作新值生成器。同样,你可以用limit方法显式限制流的大小,否则流将会无限长。

    你可能想知道,generate方法还有什么用途。我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。
    举个例子,我们将展示如何利用generate创建斐波纳契数列,这样你就可以和用iterate方法比较一下。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用!

    我们在这个例子中会使用IntStream说明避免装箱操作的代码。IntStream的generate方法会接受一个IntSupplier,而不是Supplier< t>。例如,可以这样来生成一个全是1的无限流:

    IntStream ones = IntStream.generate(() -> 1);
    
    • 1

    你也可以像下面这样,通过实现IntSupplier接口中定义的getAsInt方法显式传递一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):

    IntStream twos = IntStream.generate(new IntSupplier(){
    	public int getAsInt(){
    		return 2;
    	}
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    前面的代码创建了一个IntSupplier的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。getAsInt在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组你应该始终采用不变的方法,以便并行处理流,并保持结果正确
    请注意,因为你处理的是一个无限流,所以必须使用limit操作来显式限制它的大小;否则,终端操作(这里是forEach)将永远计算下去。同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!

  • 相关阅读:
    【Hive】Hive怎么写自定义函数(UDF、UDTF、UDAF)
    二分查找常见需求(持续更新中)
    基于C#实现的在线聊天室的桌面系统软件
    天花板级别的python读取文件方法,真的香.......
    统信系统CEF项目研发环境构建
    携手聚力 共赢数智未来丨物通博联招募生态合作伙伴
    使用Spring的StopWatch类优雅打印方法执行耗时
    Flutter 中使用 extension 使项目更具可读性和效率 01
    localForage.js
    深度解析:为何在 SwiftUI 视图的 init 初始化器里无法更改 @State 的值?
  • 原文地址:https://blog.csdn.net/qq_36602071/article/details/126865416