比尔·盖茨曾经说过:“我选择一个懒惰的人去做一件困难的事情,因为一个懒惰的人会找到一个简单的方法来做。” 关于流,没有什么比这更真实了。 在本文中,您将学习Stream如何通过在调用终端操作之前不对源元素执行任何计算来避免不必要的工作,以及源如何只生成最少数量的元素。
本文是五分之三,以GitHub存储库为补充,其中包含每个单元的说明和练习。
第1部分:创建流
第2部分:中级操作 第三部分:终端操作 第4部分:数据库流 第5部分:使用流创建数据库应用程序
终端机操作
现在我们熟悉Stream管道的初始化和构造,我们需要一种处理输出的方法。 终端操作通过从其余元素(例如
count()
)或副作用(例如
forEach(Consumer)
)。
在启动终端操作之前,Stream将不会对源的元素执行任何计算。 这意味着仅在需要时才使用源元素,这是避免不必要工作的明智方法。 这也意味着一旦应用了终端操作,流将被消耗,并且无法再添加其他操作。
让我们看一下可以应用于Stream管道末尾的哪些终端操作:
ForEach和ForEachOrdered
流的可能用例可能是更新某些或所有元素的属性,或者为什么不只是出于调试目的而将它们打印出来。 无论哪种方式,我们都不希望收集或计数输出,而是通过产生副作用而不返回值来进行。
这是目的
forEach()
或
forEachOrdered()
。 他们俩都 Consumer
并终止Stream,而不返回任何内容。 这些操作之间的区别仅仅是 forEachOrdered()
承诺按照元素在Stream中出现的顺序调用提供的Consumer。 forEach()
仅承诺以任何顺序调用Consumer。 后一种变体对并行流很有用。
在下面的简单情况下,我们在一行中打印出Stream的每个元素。
Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , “Lion” ) .forEachOrdered(System.out::print);
这将产生以下输出:
MonkeyLionGiraffeLemurLion
<br>
收集元素
Streams的常见用法是构建元素的“存储桶”,或更具体地说,构建包含特定元素集合的数据结构。 这可以通过调用终端操作来完成
Stream末尾的collect()
,因此要求它将元素收集到给定的数据结构中。 我们可以提供称为
Collector
collect()
操作,可以根据手头的问题使用许多不同的预定义类型。 以下是一些非常有用的选项:
收集到设置
我们可以将所有元素收集到
通过使用收集器收集Stream的元素来简单地进行Set
toSet()
。
Set<String> collectToSet = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .collect(Collectors.toSet());
toSet: [Monkey, Lion, Giraffe, Lemur]
收集到清单
同样,可以将元素收集到
List
使用
toList()
收集器。
List<String> collectToList = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .collect(Collectors.toList());
collectToList: [Monkey, Lion, Giraffe, Lemur, Lion]
收集到一般收藏
在更一般的情况下,可以将Stream的元素收集到任何
通过仅提供所需构造函数的Collection
Collection
类型。 构造函数的例子是 LinkedList::new
, LinkedHashSet::new
和 PriorityQueue::new
LinkedList<String> collectToCollection = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .collect(Collectors.toCollection(LinkedList:: new ));
collectToCollection: [Monkey, Lion, Giraffe, Lemur, Lion]
收集到阵列
由于数组是固定大小的容器,而不是灵活的容器 Collection
,有充分的理由进行特殊的终端操作, toArray()
,以将元素创建并存储在Array中。 请注意,仅调用toArray()
会导致Objects
Array
,因为该方法无法自行创建类型化数组。 下面我们展示如何使用String
数组的构造函数来提供类型化的数组String[]
。
String[] toArray = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .toArray(String[]:: new );
toArray: [Monkey, Lion, Giraffe, Lemur, Lion]
收集到地图
我们可能想从元素中提取信息,并将结果提供为Map
。 为此,我们使用收集器toMap()
,它需要两个
Functions
按键对应的映射器和值映射器。
该示例显示了不同的动物如何与它们名称中不同字符的数量相关联。 我们使用中间操作distinct()
来确保仅在Map
添加唯一键(如果键不是唯一的,则必须提供toMap()
收集器的变体,其中必须提供用于合并的解析器来自相等键的结果)。
Map<String, Integer> toMap = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .distinct() .collect(Collectors.toMap( Function.identity(), //Function<String, K> keyMapper s -> ( int ) s.chars().distinct().count() // Function<String, V> valueMapper ));
toMap: {Monkey= 6 , Lion= 4 , Lemur= 5 , Giraffe= 6 } (*)
(*)请注意,键顺序是未定义的。
收集分组
坚持使用桶的类比,我们实际上可以同时处理多个桶。 有一个非常有用的Collector
名为
groupingBy()
根据某些属性将元素划分为不同的组,从而通过称为“分类器”的某种内容提取该属性。 这样的操作的输出是Map
。 下面我们演示如何根据动物的名字的首字母对动物进行分组。
Map<Character, List<String>> groupingByList = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .collect(Collectors.groupingBy( s -> s.charAt( 0 ) // Function<String, K> classifier ));
groupingByList: {G=[Giraffe], L=[Lion, Lemur, Lion], M=[Monkey]}
使用下游收集器收集分组
在前面的示例中,默认情况下,将“下游收集器” toList()
应用于Map
的值,将每个存储桶的元素收集到List
。 有一个重载版本的groupingBy()
,它允许使用自定义的“下游收集器”来更好地控制生成的Map
。 下面是一个示例,说明如何将特殊的下游收集器counting()
用于计数(而不是收集)每个存储区的元素。
Map<Character, Long> groupingByCounting = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .collect(Collectors.groupingBy( s -> s.charAt( 0 ), // Function<String, K> classifier counting() // Downstream collector ));
groupingByCounting: {G= 1 , L= 3 , M= 1 }
这是该过程的说明:
任何收集器都可以用作下游收集器。 特别是,值得注意的是,收集器groupingBy()
可以采用下游收集器,该下游收集器也是groupingBy()
收集器,从而允许对第一分组操作的结果进行二次分组。 在我们的动物案例中,我们也许可以创建一个Map<Character, Map<Character, Long>>
,其中第一个地图包含具有第一个字符的键,第二个地图包含第二个字符作为键,出现次数作为值。
元素的出现
中间操作filter()
是消除与给定谓词不匹配的元素的好方法。 尽管在某些情况下,我们只是想知道是否存在至少一个满足该谓词的元素。 如果是这样,使用anyMatch()
会更方便和有效。 在这里,我们寻找数字2的出现:
boolean containsTwo = IntStream.of( 1 , 2 , 3 ).anyMatch(i -> i == 2 );
containsTwo: true
计算操作
几个终端操作输出计算结果。 我们可以执行的最简单的计算是count()
,它可以应用于任何
Stream.
例如,它可以用于计算动物数量:
long nrOfAnimals = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .count();
nrOfAnimals: 4
虽然,某些终端操作仅适用于我们在第一篇文章中提到的特殊Stream实现。 IntStream
,
LongStream
和DoubleStream
。 可以访问此类流,我们可以简单地将所有元素汇总如下:
int sum = IntStream.of( 1 , 2 , 3 ).sum();
sum: 6
或者为什么不使用.average()
计算整数的平均值:
OptionalDouble average = IntStream.of( 1 , 2 , 3 ).average();
average: OptionalDouble[ 2.0 ]
或使用.max()
检索最大值。
int max = IntStream.of( 1 , 2 , 3 ).max().orElse( 0 );
max: 3
像average()
一样, max()
运算符的结果是Optional
,因此通过声明.orElse(0)
我们自动检索该值(如果存在或默认为0)。 如果我们宁愿处理原始返回类型,也可以将相同的解决方案应用于平均示例。
如果我们对所有这些统计数据都感兴趣,那么创建几个相同的流并对每个流应用不同的终端操作是非常麻烦的。 幸运的是,有一个方便的操作称为summaryStatistics()
,它允许将几个常见的统计属性合并到一个
SummaryStatistics
对象。
IntSummaryStatistics statistics = IntStream.of( 1 , 2 , 3 ).summaryStatistics();
statistics: IntSummaryStatistics{count= 3 , sum= 6 , min= 1 , average= 2.000000 , max= 3 }
练习题
希望您现在熟悉所提供练习的格式。 如果您只是发现了该系列或者最近才感到有点懒惰(也许您也有自己的理由),我们建议您克隆GitHub存储库并开始使用后续材料。 本文的内容足以解决名为MyUnit3Terminal
的第三个单元。 相应的Unit3Terminal
接口包含JavaDocs,它们描述MyUnit3Terminal
方法的预期实现。
public interface Unit3Terminal { /** * Adds each element in the provided Stream * to the provided Set. * * An input stream of ["A", "B", "C"] and an * empty input Set will modify the input Set * to contain : ["A", "B", "C"] * * @param stream with input elements * @param set to add elements to */ void addToSet(Stream stream, Set set);
<br>
提供的测试(例如Unit3MyTerminalTest)将充当自动评分工具,让您知道您的解决方案是否正确。
下一篇
下一篇文章将展示如何将到目前为止我们积累的所有知识应用于数据库查询。
提示:再见SQL,Hello Streams…直到那时–编码愉快!
s
Per Minborg
Julia·古斯塔夫森(Julia Gustafsson)
翻译自: https://www.javacodegeeks.com/2019/10/become-master-java-streams-terminal-operations.html