collection, collections, collect, collector, collectos
collection是java集合的祖先接口。
collections是java.util包下的一个工具类,内涵各种处理集合的静态方法。
java.util.stream.stream#collect(java.util.stream.collector<? super t,a,r>)是stream的一个函数,负责收集流。
java.util.stream.collector 是一个收集函数的接口, 声明了一个收集器的功能。
java.util.comparators则是一个收集器的工具类,内置了一系列收集器实现。
收集器的作用
你可以把java8的流看做花哨又懒惰的数据集迭代器。他们支持两种类型的操作:中间操作(e.g. filter, map)和终端操作(如count, findfirst, foreach, reduce). 中间操作可以连接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗类,产生一个最终结果。collect就是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的collector接口来定义的。
预定义的收集器
下面简单演示基本的内置收集器。模拟数据源如下:
1
2
3
4
5
6
7
8
9
10
11
|
final arraylist<dish> dishes = lists.newarraylist( new dish( "pork" , false , 800 , type.meat), new dish( "beef" , false , 700 , type.meat), new dish( "chicken" , false , 400 , type.meat), new dish( "french fries" , true , 530 , type.other), new dish( "rice" , true , 350 , type.other), new dish( "season fruit" , true , 120 , type.other), new dish( "pizza" , true , 550 , type.other), new dish( "prawns" , false , 300 , type.fish), new dish( "salmon" , false , 450 , type.fish) ); |
最大值,最小值,平均值
1
2
3
4
5
6
7
8
9
10
|
// 为啥返回optional? 如果stream为null怎么办, 这时候optinal就很有意义了 optional<dish> mostcaloriedish = dishes.stream().max(comparator.comparingint(dish::getcalories)); optional<dish> mincaloriedish = dishes.stream().min(comparator.comparingint(dish::getcalories)); double avgcalories = dishes.stream().collect(collectors.averagingint(dish::getcalories)); intsummarystatistics summarystatistics = dishes.stream().collect(collectors.summarizingint(dish::getcalories)); double average = summarystatistics.getaverage(); long count = summarystatistics.getcount(); int max = summarystatistics.getmax(); int min = summarystatistics.getmin(); long sum = summarystatistics.getsum(); |
这几个简单的统计指标都有collectors内置的收集器函数,尤其是针对数字类型拆箱函数,将会比直接操作包装类型开销小很多。
连接收集器
想要把stream的元素拼起来?
1
2
3
4
|
//直接连接 string join1 = dishes.stream().map(dish::getname).collect(collectors.joining()); //逗号 string join2 = dishes.stream().map(dish::getname).collect(collectors.joining( ", " )); |
tolist
1
|
list<string> names = dishes.stream().map(dish::getname).collect(tolist()); |
将原来的stream映射为一个单元素流,然后收集为list。
toset
1
|
set<type> types = dishes.stream().map(dish::gettype).collect(collectors.toset()); |
将type收集为一个set,可以去重复。
tomap
1
|
map<type, dish> bytype = dishes.stream().collect(tomap(dish::gettype, d -> d)); |
有时候可能需要将一个数组转为map,做缓存,方便多次计算获取。tomap提供的方法k和v的生成函数。(注意,上述demo是一个坑,不可以这样用!!!, 请使用tomap(function, function, binaryoperator))
上面几个几乎是最常用的收集器了,也基本够用了。但作为初学者来说,理解需要时间。想要真正明白为什么这样可以做到收集,就必须查看内部实现,可以看到,这几个收集器都是基于java.util.stream.collectors.collectorimpl,也就是开头提到过了collector的一个实现类。后面自定义收集器会学习具体用法。
自定义归约reducing
前面几个都是reducing工厂方法定义的归约过程的特殊情况,其实可以用collectors.reducing创建收集器。比如,求和
1
2
3
|
integer totalcalories = dishes.stream().collect(reducing( 0 , dish::getcalories, (i, j) -> i + j)); //使用内置函数代替箭头函数 integer totalcalories2 = dishes.stream().collect(reducing( 0 , dish::getcalories, integer::sum)); |
当然也可以直接使用reduce
1
|
optional<integer> totalcalories3 = dishes.stream().map(dish::getcalories).reduce(integer::sum); |
虽然都可以,但考量效率的话,还是要选择下面这种
1
|
int sum = dishes.stream().maptoint(dish::getcalories).sum(); |
根据情况选择最佳方案
上面的demo说明,函数式编程通常提供了多种方法来执行同一个操作,使用收集器collect比直接使用stream的api用起来更加复杂,好处是collect能提供更高水平的抽象和概括,也更容易重用和自定义。
我们的建议是,尽可能为手头的问题探索不同的解决方案,始终选择最专业的一个,无论从可读性还是性能来看,这一般都是最好的决定。
reducing除了接收一个初始值,还可以把第一项当作初始值
1
2
|
optional<dish> mostcaloriedish = dishes.stream() .collect(reducing((d1, d2) -> d1.getcalories() > d2.getcalories() ? d1 : d2)); |
reducing
关于reducing的用法比较复杂,目标在于把两个值合并成一个值。
1
2
3
4
|
public static <t, u> collector<t, ?, u> reducing(u identity, function<? super t, ? extends u> mapper, binaryoperator<u> op) |
首先看到3个泛型,
u是返回值的类型,比如上述demo中计算热量的,u就是integer。
关于t,t是stream里的元素类型。由function的函数可以知道,mapper的作用就是接收一个参数t,然后返回一个结果u。对应demo中dish。
?在返回值collector的泛型列表的中间,这个表示容器类型,一个收集器当然需要一个容器来存放数据。这里的?则表示容器类型不确定。事实上,在这里的容器就是u[]。
关于参数:
identity是返回值类型的初始值,可以理解为累加器的起点。
mapper则是map的作用,意义在于将stream流转换成你想要的类型流。
op则是核心函数,作用是如何处理两个变量。其中,第一个变量是累积值,可以理解为sum,第二个变量则是下一个要计算的元素。从而实现了累加。
reducing还有一个重载的方法,可以省略第一个参数,意义在于把stream里的第一个参数当做初始值。
1
2
|
public static <t> collector<t, ?, optional<t>> reducing(binaryoperator<t> op) |
先看返回值的区别,t表示输入值和返回值类型,即输入值类型和输出值类型相同。还有不同的就是optional了。这是因为没有初始值,而第一个参数有可能是null,当stream的元素是null的时候,返回optional就很意义了。
再看参数列表,只剩下binaryoperator。binaryoperator是一个三元组函数接口,目标是将两个同类型参数做计算后返回同类型的值。可以按照1>2? 1:2来理解,即求两个数的最大值。求最大值是比较好理解的一种说法,你可以自定义lambda表达式来选择返回值。那么,在这里,就是接收两个stream的元素类型t,返回t类型的返回值。用sum累加来理解也可以。
上述的demo中发现reduce和collect的作用几乎一样,都是返回一个最终的结果,比如,我们可以使用reduce实现tolist效果:
1
2
3
4
5
6
7
8
9
10
11
12
|
//手动实现tolistcollector --- 滥用reduce, 不可变的规约---不可以并行 list<integer> calories = dishes.stream().map(dish::getcalories) .reduce( new arraylist<integer>(), (list<integer> l, integer e) -> { l.add(e); return l; }, (list<integer> l1, list<integer> l2) -> { l1.addall(l2); return l1; } ); |
关于上述做法解释一下。
1
2
3
|
<u> u reduce(u identity, bifunction<u, ? super t, u> accumulator, binaryoperator<u> combiner); |
u是返回值类型,这里就是list
bifunction<u, ? super t, u> accumulator是是累加器,目标在于累加值和单个元素的计算规则。这里就是list和元素做运算,最终返回list。即,添加一个元素到list。
binaryoperator<u> combiner是组合器,目标在于把两个返回值类型的变量合并成一个。这里就是两个list合并。
这个解决方案有两个问题:一个是语义问题,一个是实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变归约。相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的list。错误的语义来使用reduce方法还会造成一个实际问题:这个归约不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏list本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的list,而对象分配又会影响性能。这就是collect适合表达可变容器上的归约的原因,更关键的是它适合并行操作。
总结:reduce适合不可变容器归约,collect适合可变容器归约。collect适合并行。
分组
数据库中经常遇到分组求和的需求,提供了group by原语。在java里, 如果按照指令式风格(手动写循环)的方式,将会非常繁琐,容易出错。而java8则提供了函数式解法。
比如,将dish按照type分组。和前面的tomap类似,但分组的value却不是一个dish,而是一个list。
1
|
map<type, list<dish>> dishesbytype = dishes.stream().collect(groupingby(dish::gettype)); |
这里
1
2
|
public static <t, k> collector<t, ?, map<k, list<t>>> groupingby(function<? super t, ? extends k> classifier) |
参数分类器为function,旨在接收一个参数,转换为另一个类型。上面的demo就是把stream的元素dish转成类型type,然后根据type将stream分组。其内部是通过hashmap来实现分组的。groupingby(classifier, hashmap::new, downstream);
除了按照stream元素自身的属性函数去分组,还可以自定义分组依据,比如根据热量范围分组。
既然已经知道groupingby的参数为function, 并且function的参数类型为dish,那么可以自定义分类器为:
1
2
3
4
5
6
7
8
9
|
private caloriclevel getcaloriclevel(dish d) { if (d.getcalories() <= 400 ) { return caloriclevel.diet; } else if (d.getcalories() <= 700 ) { return caloriclevel.normal; } else { return caloriclevel.fat; } } |
再传入参数即可
1
2
|
map<caloriclevel, list<dish>> dishesbylevel = dishes.stream() .collect(groupingby( this ::getcaloriclevel)); |
多级分组
groupingby还重载了其他几个方法,比如
1
2
3
|
public static <t, k, a, d> collector<t, ?, map<k, d>> groupingby(function<? super t, ? extends k> classifier, collector<? super t, a, d> downstream) |
泛型多的恐怖。简单的认识一下。classifier还是分类器,就是接收stream的元素类型,返回一个你想要分组的依据,也就是提供分组依据的基数的。所以t表示stream当前的元素类型,k表示分组依据的元素类型。第二个参数downstream,下游是一个收集器collector. 这个收集器元素类型是t的子类,容器类型container为a,reduction返回值类型为d。也就是说分组的k通过分类器提供,分组的value则通过第二个参数的收集器reduce出来。正好,上个demo的源码为:
1
2
3
4
|
public static <t, k> collector<t, ?, map<k, list<t>>> groupingby(function<? super t, ? extends k> classifier) { return groupingby(classifier, tolist()); } |
将tolist当作reduce收集器,最终收集的结果是一个list<dish>, 所以分组结束的value类型是list<dish>。那么,可以类推value类型取决于reduce收集器,而reduce收集器则有千千万。比如,我想对value再次分组,分组也是一种reduce。
1
2
3
4
5
6
7
8
9
10
11
|
//多级分组 map<type, map<caloriclevel, list<dish>>> bytypeandcalory = dishes.stream().collect( groupingby(dish::gettype, groupingby( this ::getcaloriclevel))); bytypeandcalory.foreach((type, bycalory) -> { system.out.println( "----------------------------------" ); system.out.println(type); bycalory.foreach((level, dishlist) -> { system.out.println( "\t" + level); system.out.println( "\t\t" + dishlist); }); }); |
验证结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
---------------------------------- fish diet [dish(name=prawns, vegetarian= false , calories= 300 , type=fish)] normal [dish(name=salmon, vegetarian= false , calories= 450 , type=fish)] ---------------------------------- meat fat [dish(name=pork, vegetarian= false , calories= 800 , type=meat)] diet [dish(name=chicken, vegetarian= false , calories= 400 , type=meat)] normal [dish(name=beef, vegetarian= false , calories= 700 , type=meat)] ---------------------------------- other diet [dish(name=rice, vegetarian= true , calories= 350 , type=other), dish(name=season fruit, vegetarian= true , calories= 120 , type=other)] normal [dish(name=french fries, vegetarian= true , calories= 530 , type=other), dish(name=pizza, vegetarian= true , calories= 550 , type=other)] |
总结:groupingby的核心参数为k生成器,v生成器。v生成器可以是任意类型的收集器collector。
比如,v生成器可以是计算数目的, 从而实现了sql语句中的select count(*) from table a group by type
1
2
3
4
|
map<type, long > typescount = dishes.stream().collect(groupingby(dish::gettype, counting())); system.out.println(typescount); ----------- {fish= 2 , meat= 3 , other= 4 } |
sql查找分组最高分select max(id) from table a group by type
1
2
|
map<type, optional<dish>> mostcaloricbytype = dishes.stream() .collect(groupingby(dish::gettype, maxby(comparator.comparingint(dish::getcalories)))); |
这里的optional没有意义,因为肯定不是null。那么只好取出来了。使用collectingandthen
1
2
3
|
map<type, dish> mostcaloricbytype = dishes.stream() .collect(groupingby(dish::gettype, collectingandthen(maxby(comparator.comparingint(dish::getcalories)), optional::get))); |
到这里似乎结果出来了,但idea不同意,编译黄色报警,按提示修改后变为:
1
2
3
|
map<type, dish> mostcaloricbytype = dishes.stream() .collect(tomap(dish::gettype, function.identity(), binaryoperator.maxby(comparingint(dish::getcalories)))); |
是的,groupingby就变成tomap了,key还是type,value还是dish,但多了一个参数!!这里回应开头的坑,开头的tomap演示是为了容易理解,真那么用则会被搞死。我们知道把一个list重组为map必然会面临k相同的问题。当k相同时,v是覆盖还是不管呢?前面的demo的做法是当k存在时,再次插入k则直接抛出异常:
1
2
|
java.lang.illegalstateexception: duplicate key dish(name=pork, vegetarian= false , calories= 800 , type=meat) at java.util.stream.collectors.lambda$throwingmerger$ 0 (collectors.java: 133 ) |
正确的做法是提供处理冲突的函数,在本demo中,处理冲突的原则就是找出最大的,正好符合我们分组求最大的要求。(真的不想搞java8函数式学习了,感觉到处都是性能问题的坑)
继续数据库sql映射,分组求和select sum(score) from table a group by type
1
2
|
map<type, integer> totalcaloriesbytype = dishes.stream() .collect(groupingby(dish::gettype, summingint(dish::getcalories))); |
然而常常和groupingby联合使用的另一个收集器是mapping方法生成的。这个方法接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接收特定类型元素的收集器适应不同类型的对象。我么来看一个使用这个收集器的实际例子。比如你想得到,对于每种类型的dish,菜单中都有哪些caloriclevel。我们可以把groupingby和mapping收集器结合起来,如下所示:
1
2
|
map<type, set<caloriclevel>> caloriclevelsbytype = dishes.stream() .collect(groupingby(dish::gettype, mapping( this ::getcaloriclevel, toset()))); |
这里的toset默认采用的hashset,也可以手动指定具体实现tocollection(hashset::new)
分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称为分区函数。分区函数返回一个布尔值,这意味着得到的分组map的键类型是boolean,于是它最多可以分为两组:true or false. 例如,如果你是素食者,你可能想要把菜单按照素食和非素食分开:
1
|
map< boolean , list<dish>> partitionedmenu = dishes.stream().collect(partitioningby(dish::isvegetarian)); |
当然,使用filter可以达到同样的效果:
1
|
list<dish> vegetariandishes = dishes.stream().filter(dish::isvegetarian).collect(collectors.tolist()); |
分区相对来说,优势就是保存了两个副本,当你想要对一个list分类时挺有用的。同时,和groupingby一样,partitioningby一样有重载方法,可以指定分组value的类型。
1
2
3
4
5
6
7
|
map< boolean , map<type, list<dish>>> vegetariandishesbytype = dishes.stream() .collect(partitioningby(dish::isvegetarian, groupingby(dish::gettype))); map< boolean , integer> vegetariandishestotalcalories = dishes.stream() .collect(partitioningby(dish::isvegetarian, summingint(dish::getcalories))); map< boolean , dish> mostcaloricpartitionedbyvegetarian = dishes.stream() .collect(partitioningby(dish::isvegetarian, collectingandthen(maxby(comparingint(dish::getcalories)), optional::get))); |
作为使用partitioningby收集器的最后一个例子,我们把菜单数据模型放在一边,来看一个更加复杂也更为有趣的例子:将数组分为质数和非质数。
首先,定义个质数分区函数:
1
2
3
4
|
private boolean isprime( int candidate) { int candidateroot = ( int ) math.sqrt(( double ) candidate); return intstream.rangeclosed( 2 , candidateroot).nonematch(i -> candidate % i == 0 ); } |
然后找出1到100的质数和非质数
1
2
|
map< boolean , list<integer>> partitionprimes = intstream.rangeclosed( 2 , 100 ).boxed() .collect(partitioningby( this ::isprime)); |
原文链接:https://www.cnblogs.com/woshimrf/p/java8-collect-stream.html