收集器简介 Collector
函数式编程相对于指令式编程的一个主要优势:你只需要指出希望的结果“做什么”,而不用操心执行的步骤“如何做”。
收集器用作高级规约
函数式API设计的另一个好处:更容易复合和重用。收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体的说,对流调用collect方法将对流中的元素触发一个规约操作(由Coolector来参数化)。
List transactions = transactionStream.collect(Collectors.toList());
规约和汇总
利用counting工厂方法返回收集器,数一数菜单里有多少种菜:
long howManyDishes = menu.stream().collect(Collectors.counting());
还可以这样写更为直接:
long howManyDishes = menu.stream().count();
查找流中的最大值和最小值 maxBy minBy
找出菜单中热量最高的菜。你可以使用两个收集器,Collectors.maxBy()和Collectors.minBy()
你可以创建一个Comparator来根据所含热量对菜肴进行比较,并把Collectors.maxBy:
Optional mostCalorieDish = menu.stream().collect(maxBy((a1,a2)—>a1.getCalories - a2.getCalories));
汇总 summingInt summingDouble
Collectors类专门为汇总提供了一个工厂方法:Collectors.summintInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。举个例子,计算菜单列表的总热量:
int totalCalories = menu.stream().collect(summingInt((v)-> v.getCalories()));
汇总不仅仅求和;还有Collectors.averageingInt,连同对应的averagingInt等计算数值的平均数。
int averageCalories = menu.stream().collect(averagintInt((a) -> a.getCalories()));
到目前为止,已经看到了如何使用手机器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。这种情况下,你可以就输出菜单中的元素个数,并得到菜肴热量的总和,平均值,最大值和最小值。
IntSummaryStatastics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
这个收集器会把所有这些信息收集到一个叫做IntSummaryStatistics的类里面,他提供了方便的取值(getter)方法来访问结果。打印menuStatisticobject会得到下面的结果:
IntSummaryStatistics{count=9,sum=4300,min=120,average=47.7778,max=800}
连接成字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。这意味着你把菜单中所有的菜肴的名称连接起来。
String shortMenu = menu.stream().map(Dish:getName).collect(joining());
请注意,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴的名称,那么你无需提取每一道菜名称的函数来对原流做映射就可以得到相同的结果:
String shortMenu = menu.stream().collect(jioning());
但该字符串的可读性并不好。幸好,joining的工厂方法有一个重载版本可以接受元素之间的分解符,这样你就可以得到一个逗号分隔符的菜肴名称列表:
String shortMenu = menu.stream().map(Dish::getname()).collect(joining(", "));
到目前为止,我们已经探讨了各种将流归约到一个值的收集器。在下一节中,我们会展示为什么所有这种形式的归约过程,其实都是Collectors.reducing工厂方法提供的更广义归约收集器的特殊的情况。
广义的归约汇总
事实上,我们已经讨论的所有的收集器,都是一个可以用reducing的工厂方法定义的归约过程的特殊情况二期。Collectors.reducing工厂方法是所有这些特殊情况的一般化。列如,可以用reducing方法创建的收集器来计算你菜单的总热量,如下:
int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,(i,j)->i+j));
同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示:
Optional mostCalorieDish = menu.stream().collect(reducing((d1,d2)_.d1.getCalories()>d2.getCalories()?d1:d2));
分组
一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。就像按货币对交易进行分组,如果用指令式风格来实现的话,这个操作可能会很麻烦,啰嗦,容易出错。但是用java8的话很容易看懂。举一个列子:假设要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的放一组。用Collectors.groupingBy工厂方法返回的收集器就可以轻松的完成这项任务。
Map> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
这里,你给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.Type。我们把这个Function叫做分类函数,因为它用来把流中的元素分成不同的组。分组结果时一个Map,把分组函数返回的值座位映射的建,把流中所有具有这个分类值的项目的列表座位对应的映射值。在菜单分裂的例子中,键就是菜的类型,值就是包含所有对应类型的菜肴列表。
多级分组
要实现多级分组,我们可以使用一个右双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以吧一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。
Map>> dishesByTypeCaloricLevel = menu.stream().collect(groupingBy(Dish::getType,groupingBy(dish ->{
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <=700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT})));
分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。例如,如果你是素食这或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:
Map> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
那么通过Map中键位true的值,就可以找出所有的素食菜肴了:
List vegetarianDishes = partitionedMenu.get(true);
收集器接口 Collector
Collector接口包含了一系列方法,为实现具体的归约操作提供了范本。我们已经看过了Collector接口中实现的许多收集器,列如toList或者groupingBy.这也意味着,你可以为Collector接口提供自己的实现,从而自由地创建自定义归约操作。
Collector接口定义
public interface Collector{
Suppier supplier();
BiConsumer accumulator();
Function finisher();
BinaryOperator combiner();
Set characteristics();
}
T :是流要收集的项目的泛型。
A :是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
R :是手机操作得到的对象(通常但并不一定是集合)的类型。
例如,你可以实现一个ToListCollector
public class ToListCollector implements Collector,List>
理解Collector接口声明的方法
现在我们可以一个一个来分析Collector接口声明的五个方法了。通过分析,你会注意到,前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用那些优化(比如并行化)
建立新的结果容器:supplier方法
supplier方法必须返回一个结果为空Suppier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收集器,比如我们的ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的ToListCollector中,supplier返回一个空的List
public Supplier> supplier(){
return ()->new ArrayList();
}
请注意你也可以值传递一个构造函数引用:
public Supplier> supplier(){
return ArrayList::new;
}
将元素添加到结果容器:accumulator方法
accumulator方法会返回执行归约操作的函数。当遍历到流中的第n个元素是,这个函数执行时会有两个参数:保存归约结果的累加器(已经收集了流中的前n-1个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素效果。对于ToListCollector,这个函数仅仅会把当前项目添加至已经遍历过的项目的列表:
public BiConsumer,T> accumulator(){
return (list,item) -> list.add(item);
}
也可以使用方法引用,这会更简洁:
public BiConsumer,T> accumulator(){
return List::add;
}
对结果容器应用最终转化:finisher方法
在遍历完流后,finisher方法必须返回在累积过程的最后要调用一个函数,以便将累加器对象转换为整个集合操作的最终效果。通常,就像ToListCollector的情况一些样,累加器对象恰好复合预期的最终效果,因此无需转换。所以finisher方法只需返回identity函数:
public Funciton,List> finisher(){
reutrn Function.identity();
}
合并两个结果容器:combiner方法
四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对于toList而言,这个方法的实现非常简单,只要把从流的第二部分收集到的项目列表加到遍历第一部分时得到的列表后面就行了:
public BinaryOperator> combiner(){
return (list1,list2)->{
list1.addAll(list2);
return list1;
}
}
- 原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非(如果分布式工作单位太小,并行计算往往比顺序计算要慢,而且要是生成的并行任务比处理器内核书多的话就毫无意义了)
- 现在,所有的子流都可以并行处理,即对每个子流应用上图的顺序归约算法
- 最后,使用收集器combiner方法返回函数,将所有的部分结果两两合并。这时会把原始流每次拆分时得到的子流对应的结果和并起来。
characteristics方法
最后一个方法——characteriestics会返回一个不可表你的Characteristics集合,它定义了收集器的行为——尤其是关于可以并行归约,以及可以使用那些优化的提示。Characteristics是一个包含三个项目的枚举。
- UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
- CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有表为UNORDERED,那它尽在用于无需数据源时才可以并行归约。
- IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查的转化为结果R是安全的。
ToListCollector是IDENTITY_FINISH的,因为用来累积流中元素的List已经是我们要的最终结果,用不着进一步转换了,但他并不是UNORDERED,因为用在有序留上的时候,我们还是希望顺序能够保留在得到的List中。最后,他是CONCURRENT的,但我们刚才说过了,仅仅在背后的数据源无序时才会进行并行处理