筛选各异的元素
流还支持一个叫作aistinct的方法,它会返回一个元素各异(根据流所生成元素的hashcode和eguals方法实现)的流。例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。图5-2直观地显示了这个过程。
-
- List<Integer>numbers=arrays.asList(1,2,1,3,3,2,4);
- numbers.stream()
- .filter(i->i2 == 0)
- .distinct()
- .forEach(System.out::println);
5.1.3截短流
流支持Limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给Limit。如果流是有序的,则最多会返回前n个元素。比如,你可以建立一个ist,选出热量
超过300卡路里的头三道菜:
-
- List<Dish>dishes =menu.stream()
- .filterid ->d.getCalories()>300)
- .limit(3)
- .collectitoList())
图5-3展示了filter和limit的组合。你可以看到,该方法只选出了符合谓词的头三个元素然后就立即返回了结果。
请注意limit也可以用在无序流上,比如源是一个set。这种情况下,limit的结果不会以
任何顺序排列
5.1.4 跳过元素
流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。请注意,1imit(n)和skip(n)是互补的!例如,下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的。图5-4展示了这个查询。
- List<Dish>dishes =menu.stream()
- .filter(d ->d.getCalories()>300)
- .skip(2)
- .collect(toList());
在我们讨论映射操作之前,在测验5.1上试试本节学过的内容吧
测验5.1:筛选
你将如何利用流来筛选前两个荤菜呢?
答案:你可以把filter和limit复合在一起来解决这个问题,并用collect(toList())将流转换成一个列表。
- List<Dish> dishes = menu.stream()
- .filter(d ->d.getType()== Dish.Type.MEAT)
- .limit(2).collect(toList());
5.2 映射
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。StreamAPI也通过map和flatMap方法提供了类似的工具。
5.2.1 对流中每一个元素应用函数
流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。例如,下面的代码把方法引用Dish::getName传给了map方法,来提取流中菜看的名称:
- List<String>dishNames = menu.stream()
- .map(Dish::getName )
- .collect(toList());
因为getName方法返回一个string,所以map方法输出的流的类型就是stream
- List<String>words = Arrays.asList("Java 8","Lambdas","In","Action");
- List<Integer> wordLengths =words.stream()
- .map(String::length)
- .collect(toList());
现在让我们回到提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做?你可以像下面这样,再链接上一个map:
- List<Integer>dishNameLengths =menu.stream()
- .map(Dish::getName)
- .map(String::length
- .collect(toList());
5.2.2 流的扁平化
你已经看到如何使用map方法返回列表中每个单词的长度了。让我们拓展一下:对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表["Hello""world"],你想要返回列表["H","e","l",“l","o",“W”,“o”,“r”,"l","d"]
你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用aistinct来过滤重复的字符。第一个版本可能是这样的:
- words.stream()
- .map(word -> word.split("")).distinet()
- .collect(toList())i
这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个string[](string列表)。因此,map返回的流实际上是stream
1.尝试使用map和Arrays.stream()
首先,你需要一个字符流,而不是数组流。有一个叫作Arrays.stream()的方法可以接受一个数组并产生一个流,例如:
- StringllarrayOfWords ={"Goodbye","World"};
- Stream<String>streamOfwords=Arrays.stream(arrayOfWords);
把它用在前面的那个流水线里,看看会发生什么:
- words .stream()
- .map(word -> word.split(""))// 将每个单词转换为由其字母构成的数组
- .map(Arrays::stream)//让每个数组变成
- .distinct()//一个单独的流
- .collect(toList())
当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表(更准确地说是stream
2.使用flatMap
你可以像下面这样使用flatMap来解决这个问题:
- List<String>uniqueCharacters=
- words.stream()
- .map(w -> w.split(""))
- .flatMap(Arrays::stream)
- .distinct()
- .collecticollectors.toList());
将每个单词转换为由其字母构成的数组
将各个生成流扁平化为单个流
使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。图5-6说明了使用flatMap方法的效果。把它和图5-5中map的效果比较一下
一言以蔽之,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。在第10章,我们会讨论更高级的Java 8模式,比如使用新的optiona1类进行nu11检查时会再来看看flatMap。为巩固你对于map和flatMap的理解,
试试测验5.2吧
测验5.2:映射
(1) 给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1,2,3,4,5],应该返回[1,4,9.16,25]。
答案:你可以利用map方法的Lambda,接受一个数字,并返回该数字平方的Labda来解决这个问题。
- List<Integer>numbers=arrays.asList(1,2,3,4,5);
- List<Integer>squares
- numbers.stream()
- .map(n->n*n)
- .collect(toList());
(2) 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1,2,3]和列表[3,4],应该返回[(1,3),(1,4),(2,3),(2,4),(3,3),(3,4)]。为简单起见,你可以用有两个元素的数组来代表数对。
答案:你可以使用两个map来迭代这两个列表,并生成数对。但这样会返回一个stream-
- List<Integer>numbersl=arrays.asList(1,2,3);
- List<Integer>numbers2=arrays.asList(3,4);
- List<int]> pairs = numbersl.stream()
- .flatMap(i ->numbers2.stream()
- .map(j->new int[]{i,j})
- .collect(toList());
(3)如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2,4)和(3,3)是可以的。答案:你在前面看到了,filter可以配合谓词使用来筛选流中的元素。因为在flatMap操作后,你有了一个代表数对的int[]流,所以你只需要一个谓词来检查总和是否能被3整除就可以了:
- List<Integer> numbersl=Arrays.asList(1,2,3);
- List<Integer>numbers2=arrays.asList(3,4);
- List<int]> pairs =numbersl.stream()
- .flatMap(i->numbers2.stream()
- .filter(j->(i+j)%3 == 0)
- .map(j-> new int[]{i,j})
- .collect(toList());
其结果是[(2,4),(3,3)]。
5.3查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。StreamAPI通过allMatch、anyMatch、noneMatch、findrirst利findAny方法提供了这样的工具。
5.3.1 检查谓词是否至少匹配一个元素
anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。比如,你可以用它来看看菜单里面是否有素食可选择:
- if(menu.stream().anyMatch(Dish::isVegetarian))
- {
- System.out .println("The menu is(somewhat)vegetarian friendly!!");
- }
anyMatch方法返回一个boolean,因此是一个终端操作。
5.3.2检查谓词是否匹配所有元素
allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):
- boolean isHealthy=menu.stream()
- .allMatch(d->d.getCalories()<1000);
noneMatch
和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用noneMatch重写前面的例子:
- boolean isHealthy=menu.stream()
- .noneMatch(d->d.getCalories()>=1000);
anyMatch、a11Match和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&和运算符短路在流中的版本。
短路求值
有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路,
对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。我们会在5.7节中介绍无限流的例子。
5.3.3 查找元素
fināany方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜看。你可以结合使用filter和findany方法来实现这个查询:
Optional
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。不过慢着代码里面的optional是个什么玩意儿?
optional简介
0ptional
optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错
口isPresent()将在optiona1包含值的时候返回true,否则返回false。
口 ifPresent(consumer
口т get()会在值存在时返回值,否则抛出一个Nosuchglement异常口тorElse(т other)会在值存在时返回值,否则返回一个默认值。
例如,在前面的代码中你需要显式地检査optiona1对象中是否存在一道菜可以访问其名称:
- menu.stream()
- .filter(Dish::isVegetarian)
- .findAny()
- .ifPresent(d ->System.out.println(d.getName());
如果包含一个值就打印它,否则什么都不做
5.3.4 查找第一个元素
有些流有一个出现顺序(encounterorder)来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流 )。对于这种流,你可能想要找到第一个元素。为此有一个findrirst方法,它的工作方式类似于findany。例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:
- List<Integer>someNumbers=Arrays.asList(l,2,3,4,5);
- Optional<Integer>firstSquareDivisibleByThree=
- someNumbers.stream()
- .map(x -> x *x)
- .filter(x->x3==0)
- .findFirst();//9
何时使用findrirst和findAny
你可能会想,为什么会同时有findpirst和findany呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
5.4 归约
到目前为止,你见到过的终端操作都是返回一个boolean(allMatch之类的)、void(forEach)或optiona1对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List。
在本节中,你将看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
5.4.1 元素求和
在我们研究如何使用reduce方法之前,先来看看如何使用for-each循环来对数字列表中的元素求和:
- int sum =0;
- for (int x:numbers){
- SUm +=X}
numbers中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:
口总和变量的初始值,在这里是0;
口将列表中所有元素结合在一起的操作,在这里是+。
要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce操作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:
int sum =numbers.stream().reduce(0,(a,b)->a +b);
reduce接受两个参数:
一个初始值,这里是0:
一个Binaryoperator
图5-7展示了reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。让我们深人研究一下reduce操作是如何对一个数字流求和的。首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。
你可以使用方法引用让这段代码更简洁。在Java8中,Integer类现在有了一个静态的sum方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了:
int sum =numbers.stream().reduce(0,Integer::sum);
无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个optiona1对象:
Optional
为什么它返回一个optional
5.4.2 最大值和最小值
原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的reduce来计算流中最大或最小的元素。正如你前面看到的,reduce接受两个参数:
一个初始值
一个Lammbda来把两个流元素结合起来并产生一个新值Lambda是一步步用加法运算符应用到流中每个元素上的,如图5-7所示。因此,你需要一个
给定两个元素能够返回最大值的Lambda。reduce操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce来计算流中的最大值,如图5-8所示。
- int count= menu.stream()
- .map(d ->1)
- .reduce(0,(a,b)->a+b);
第5章 使用流
map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名因为它很容易并行化。请注意,在第4章中我们也看到了内置count方法可用来计算流中元素的个数
long count=menu.stream(}.count();
归约方法的优势与并行化
相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的选代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。你在第7章会看到使用分支1合并框架来做是什么样子但现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是reduce所提供的。你还将在第7章看到,使用流来对所有的元素并行求和时,你的代码几乎不用修改:stream()换成了
- parallelstream()。
- int sum =numbers.parallelStream(),reduce(0,Integer::sum);
但要并行执行这段代码也要付一定代价,我们稍后会向你解释:传递给reduce的Lambda不能更改状态(如实例变量)而且操作必须满足结合律才可以按任意顺序执行。
到目前为止,你看到了产生一个Integer的归约例子:对流求和、流中的最大值,或是流中元素的个数。你将会在5.6节看到,诸如sum和max等内置的方法可以让常见归约模式的代码再简洁一点儿。我们会在下一章中讨论一种复杂的使用co1lect方法的归约。例如,如果你想要按类型对菜看分组,也可以把流归约成一个Map而不是Integer。
流操作:无状态和有状态
你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时候把stream换成parallelstream就可以实现并行。
当然,对于许多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用filter选出某一类的菜肴,然后对得到的流做map来对卡路里求和,最后reduce得到菜单的总热量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。
但诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。
、
相反,诸如sort或distinct等操作一开始都和filter和map差不多--都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
你现在已经看到了很多流操作,可以用来表达复杂的数据处理查询。表5-1总结了迄今讲过的操作。你可以在下一节中通过一个练习来实践一下。
5.5 付诸实践
在本节中,你会将迄今学到的关于流的知识付诸实践。我们来看一个不同的领域:执行交易
98第5章 使用流
的交易员。你的经理让你为八个查询找到答案。你能做到吗?我们在5.5.2节给出了答案,但你应该自己先尝试一下作为练习。
(1)找出2011年发生的所有交易,并按交易额排序(从低到高)。
(2)交易员都在哪些不同的城市工作过?
(3)查找所有来自于剑桥的交易员,并按姓名排序。
(4)返回所有交易员的姓名字符串,按字母顺序排序。
(5)有没有交易员是在米兰工作的?
(6)打印生活在剑桥的交易员的所有交易额。
(7)所有交易中,最高的交易额是多少?
(8)找到交易额最小的交易。
5.5.1 领域:交易员和交易
以下是你要处理的领域,一个Traders和Transactions的列表:
5.5.2 解答
解答在下面的代码清单中。你可以看看你对迄今所学知识的理解程度如何。干得不错!代码清单5-1 找出2011年的所有交易并按交易额排序(从低到高)给filter传递一个谓词
- List<Transaction> tr2011 =
- transactions.stream()
- .filter(transaction ->transaction.getYear()2011)
- .sorted(comparing(Transaction::getValue))
- .collect(toList());
来选择2011年的交易将生成的stream中的所有元素收集到一个List中按照交易额进行排序代码清单5-2 交易员都在哪些不同的城市工作过
- List<gtring> cities =
- transacti0ns.stream)
- .map(transaction>transaction.getTrader().getCity())
- .distinct()
- .collect(toList());
提取与交易相关的每位交易员的所在城市,只选择互不相同的城市
这里还有一个新招:你可以去掉aistinct(),改用toset(),这样就会把流转换为集合。
你在第6章中会了解到更多相关内容。
- Set<String> cities =
- transactions.stream()
- .map(transaction ->transaction.getTrader().getCity())
- .collect(toset());
代码清单5-3 查找所有来自于剑桥的交易员,并按姓名排序
- List<Trader>traders =transactions.stream() //从交易中提取所有交易员
- .map(Transaction::getTrader)
- .filter(trader -> trader.getCity().equals("Cambridge" ))
- //仅选择位于剑桥的交易员
- .distinct()
- .orted(comparing(Trader::getName))//照姓名进行排序
- .collect(toList());
确保没有任何重复
对生成的交易员流按
代码清单5-4返回所有交易员的姓名字符串,按字母顺序排序
- String traderstr=transactions.stream()//提取所有交易员姓名,生成一只选择个strings构成的stream.map(transaction->transaction.getTrader().getName())//不相同的姓名
- .distinct()//逐个拼接每个名字,得到一个将所有名字连接起来的string
- .sorted()//对姓名按字P
- .reduce(""。(n1。n2)-> n1 + n2)i
母顺序排序请注意,此解决方案效率不高(所有字符串都被反复连接,每次迭代的时候都要建立一个新的string对象)。下一章中,你将看到一个更为高效的解决方案,它像下面这样使用joining(其内部会用到stringBuilder):
- String traderStr=
- transactions:stream()
- .map(transaction ->transaction.getTrader().getName()).distinct()
- .sortedl)
- .collect(joining());
-
代码清单5-5 有没有交易员是在米兰工作的
- boolean milanBased =
- transactions:stream()
- anyMatch(transaction ->transaction.getTrader()
- .getCity()//把一个谓词传递给anyMatch,检查是否有交易员在米兰工作
- .equals("Milan"))
5.6数值流
我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:
- int calories =menu.stream()
- .map(Dish::getCalories)
- reduce(0,Integer::sum)
这段代码的问题是,它有一个暗含的装箱成本。每个nteger都必须拆箱成一个原始类型再进行求和。要是可以直接像下面这样调用sum方法,岂不是更好?
- int calories =menu.stream()
- .map(Dish::getCalories)
- .sum();
但这是不可能的。问题在于map方法会生成一个stream
5.6.1 原始类型流特化
Java 8引人了三个原始类型特化流接口来解决这个问题:Intstream、Doublestream和Longstream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性一-即类似int和Integer之间的效率差异。
1.映射到数值流
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是stream<>。例如,你
可以像下面这样用mapToInt对menu中的卡路里求和:
int caloriesmenstream().mapToInt(Dish::getCalories).sum()
mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个Intstream(而不是一个stream
返回一个Stream
2.转换回对象流
同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,Intstream上的操作只能产生原始整数:Intstream的map操作接受的Lambda必须接受int并返回int(一个IntUnaryoperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:
将stream转换为数值流
- IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
- Stream<Integer>stream =intStream.boxed()
将 数 值 流 转换为stream你在下一节中会看到,在需要将数值范围装箱成为一个一般流时,boxed尤其有用。
3.默认值optionalInt
求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算Intstream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了optiona1类,这是一个可以表示值存在或不存在的容器。optiona1可以用Integer、string等参考类型来参数化。对于三种原始流特化,也分别有一个optiona1原始类型特化版本:0ptionalInt、0ptionalDouble利OptionalLong。例如,要找到Intstream中的最大元素,可以调用max方法,它会返回一个0ptionalInt:
OptionalInt maxCalories =menu.stream().mapToInt(Dish::getCalories).max();
现在,如果没有最大值的话,你就可以显式处理optiona1Int去定义一个默认值了:
int max =maxCalories.orElse(1)如果没有最大值的话,显式提供一个默认最大值
5.6.2 数值范围
和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java8引人了两个可以用于Intstream和Longstream的静态方法,帮助生成这种范围:range和rangeclosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeclosea则包含结束值。让我们来看一个例子:
- 一个从1到100的偶数
- IntStream evenNumbers =IntStream.rangeClosed(1,100)
- .filter(n->n%2==0);
-
- System.out.println(evenNumbers.count());
50个偶数这里我们用了rangeclosea方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接fi1ter方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count。因为count是一个终端操作,所以它会处理流,并返回结果50,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用Intstream.range(1,100),则结果将会是49个偶数,因为range是不包含结束值的
5.6.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)都是有效的勾股数。勾股数很有用,因为它们描述的正好是直角三角形的三条边长,如图5-9所示。
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)
假设周围的代码给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)});
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).mapTo0bi(b->
- new int[]{a,b,(int)Math.sqrt(a*a+b*b)})
好的,flatMap又是怎么回事呢?首先,创建一个从1到100的数值范围来生成a的值。对每个给定的a值,创建一个三元数流。要是把a的值映射到三元数流的话,就会得到一个由流构成的流。flatmap方法在做映射的同时,还会把所有生成的三元数流扁平化成一个流。这样你就得到了一个三元数流。还要注意,我们把b的范围改成了a到100。没有必要再从1开始了,否则就会造成重复的三元数,例如(3,4,5)和(4,3,5)。
5.7 构建流
希望到现在,我们已经让你相信,流对于表达数据处理查询是非常强大而有用的。到目前为止,你已经能够使用stream方法从集合生成流了。此外,我们还介绍了如何根据数值范围创建数值流。但创建流的方法还有许多!本节将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流!
5.7.1 由值创建流
你可以使用静态方法stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再一个个打印出来:
Stream<string>stream=Stream.of("Java 8","Lambdas","In","Action");stream.map(String::toUpperCase).forEach(System.out ::println);
你可以使用empty得到一个空流,如下所示:
Stream
5.7.2 由数组创建流
你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如,你可以将一个原始类型int的数组转换成一个Intstream,如下所示:
int[]numbers ={2.3,5,7,11,13};int sum =Arrays.stream(numbers).sum()i
总和是41
5.7.4 由函数生成流:创建无限流
Stream API提供了两个静态方法来从函数生成流:stream.iterate和stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说应该使用1imit(n)来对这种流加以限制,以避免打印无穷多个值。
1.迭代
我们先来看一个iterate的简单例子,然后再解释:
- Stream.iterate(0,n->n+2)limit(10)
- forEach(System.out::println);
- iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>类型)。
这里,我们使用Lambdan->n+2,返回的是前一个元素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流--这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用1imit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。一般来说,在需要依次生成一系列值的时候应该使用iterate,比如一系列日期:1月31日2月1日,依此类推。来看一个难一点儿的应用iterate的例子,试试测验5.4。
测验5.4:斐波纳契元组序列
斐波纳契数列是著名的经典编程练习。下面这个数列就是斐波纳契数列的一部分:0,1,12,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
素。这就是iterate方法的初始值:
Stream.iterate(new int[]{0,1},???)
.1imit{20}.forEach(t ->System.out.println("("+ t[0]+ "," + t[1] +")"));在这个测验中,你需要搞清楚 代表的代码是什么。请记住,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] +")"));
它是如何工作的呢?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],[0] + t[1]})
- .limit(10).mapt -> t[0])
- .forEach(System.out::println);
这段代码将生成斐波纳契数列:0,1,1,2,3,5,8,13,21,34…
2. 生成
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个supplier
- Stream.generate(Math::random)
- .limit(5)
- .forEach(System.out::println);
这段代码将生成一个流,其中有五个0到1之间的随机双精度数。例如,运行一次得到了下面的结果:
0.9410810294106129
0.6586270755634592
0.9592859117266873
0.13743396659487006
0.3942776037651241
Math.Random静态方法被用作新值生成器。同样,你可以用1imit方法显式限制流的大小,否则流将会无限长。
你可能想知道,generate方法还有什么用途。我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。举个例子,我们将展示如何利用generate创建测验5.4中的斐波纳契数列,这样你就可以和用iterate方法的办法比较一下。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用!我们会在第7章中进一步讨论这个.操作的问题和副作用,以及并行流。
我们在这个例子中会使用Intstream说明避免装箱操作的代码。Intstream的generate方法会接受一个Intsupplier,而不是supplier
IntStream ones=Inttream.generate(()->1)i
你在第3章中已经看到,Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。你也可以像下面这样,通过实现Intsupplier接口中定义的getAsInt方法显式传递一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):
- IntStream twos =IntStream.generate(new IntSupplier(){public int getAsInt(){return 2i
- }):
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.currentithis.previous = this.current;
- this.current = nextValue;
- return oldPrevious;
- }
- Intstream.generate(fib).limit(10).forEach(System.out ::println);
前面的代码创建了一个Intsupplier的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。getasInt在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。你将在第7章了解到,你应该始终采用不变的方法,以便并行处理流,并保持结果正确。请注意,因为你处理的是一个无限流,所以必须使用1imit操作来显式限制它的大小:否则,终端操作(这里是foreach)将永远计算下去。同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!
5.8 小结
这一章很长,但是很有收获!现在你可以更高效地处理集合了。事实上,流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化。以下是你应从本章中学到的关键概念。口Streams API可以表达复杂的数据处理查询。常用的流操作总结在表5-1中。
口你可以使用filter、aistinct、skip和limit对流做筛选和切片
口你可以使用map和flatMap提取或转换流中的元素。
口你可以使用findFirst和findAny方法查找流中的元素。你可以用a1lMatch、noneMatch和anyMatch方法让流匹配给定的谓词。
口这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。口你可以利用reduce方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
口 fi1ter和map等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才能计算出一个值。sortea和aistinct等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。口流有三种基本的原始类型特化:Intstream Doublestream和Longstream。它们的操作也有相应的特化。
口流不仅可以从集合创建,也可从值、数组、文件以及iterate与generate等特定方法创建。
口无限流是没有固定大小的流。