在上一节中,我们了解到终端操作collect
方法用于收集流中的元素,并放到不同类型的结果中,比如List
、Set
或者Map
。其实collect
方法可以接受各种Collectors接口的静态方法作为参数来实现更为强大的规约操作,比如查找最大值最小值,汇总,分区和分组等等。
准备工作
为了演示Collectors接口中的静态方法使用,这里创建一个Dish类(菜谱类):
1 | public class Dish { |
然后创建一个List,包含各种食材:
1 | List<Dish> list = Arrays.asList( |
在测试类中导入所有Collectors接口的静态方法:
1 | import static java.util.stream.Collectors.*; |
规约与汇总
最大最小值
Collectors.maxBy
和Collectors.minBy
用来计算流中的最大或最小值,比如按卡路里的大小来筛选出卡路里最高的食材:
1 | list.stream() |
输出pork
。
汇总
Collectors.summingInt
可以用于求和,参数类型为int类型。相应的基本类型对应的方法还有Collectors.summingLong
和Collectors.summingDouble
。比如求所有食材的卡路里:
1 | list.stream().collect(summingInt(Dish::getCalories)); // 4200 |
Collectors.averagingInt
方法用于求平均值,参数类型为int类型。相应的基本类型对应的方法还有Collectors.averagingLong
和Collectors.averagingDouble
。比如求所有食材的平均卡路里:
1 | list.stream().collect(averagingInt(Dish::getCalories)); // 466.6666666666667 |
Collectors.summarizingInt
方法可以一次性返回元素个数,最大值,最小值,平均值和总和:
1 | IntSummaryStatistics iss = list.stream().collect(summarizingInt(Dish::getCalories)); |
同样,相应的summarizingLong
和summarizingDouble
方法有相关的LongSummaryStatistics
和DoubleSummaryStatistics
类型,适用于收集的属性是原始类型long或double的情况。
拼接
Collectors.joining
方法会把流中每一个对象应用toString
方法得到的所有字符串连接成一个字符串。如:
1 | list.stream().map(Dish::getName).collect(joining()); |
内部拼接采用了StringBuilder
。除此之外,也可以指定拼接符:
1 | list.stream().map(Dish::getName).collect(joining(",")); |
reducing
Collectors.reducing
方法可以实现求和,最大值最小值筛选,拼接等操作。上面介绍的方法在编程上更方便快捷,但reducing
的可读性更高,实际使用哪种我觉得还是看个人喜好。举个使用reducing
求最大值的例子:
1 | list.stream().collect(reducing(0, Dish::getCalories, Integer::max)); // 800 |
或者:
1 | list.stream().map(Dish::getCalories).collect(reducing(0, Integer::max)); // 800 |
分组
分组功能类似于SQL里的group by
,可以对流中的元素按照指定分组规则进行分组。
普通分组
Collectors.groupingBy
方法可以轻松的完成分组操作。比如现在对List中的食材按照类型进行分组:
1 | Map<Dish.Type, List<Dish>> dishesByType = list.stream().collect(groupingBy(Dish::getType)); |
输出结果{OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}
。
我们也可以自定义分组规则,比如按照卡路里的高低分为高热量,正常和低热量:
首先定义一个卡路里高低的枚举类型
1 | public enum CaloricLevel { DIET, NORMAL, FAT }; |
然后编写分组规则:
1 | Map<CaloricLevel, List<Dish>> dishesByCalories = list.stream().collect( |
输出结果:{DIET=[chicken, rice, season fruit, prawns], NORMAL=[beef, french fries, pizza, salmon], FAT=[pork]}
。
多级分组
Collectors.groupingBy
支持嵌套实现多级分组,比如将食材按照类型分类,然后再按照卡路里的高低分类:
1 | Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesGroup = list.stream().collect( |
返回结果是一个二级Map,输出结果{FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}, MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}}
。
实际上,第二个参数除了Collectors.groupingBy
外,也可以传递其他规约操作,规约的结果类型对应Map里的第二个泛型。举些例子,将食材按照类型分,然后统计各个类型对应的数量:
1 | Map<Dish.Type, Long> dishesCountByType = list.stream().collect(groupingBy(Dish::getType,counting())); |
因为Collectors.counting
方法返回Long类型,所以Map第二个泛型也必须指定为Long。输出结果:{OTHER=4, FISH=2, MEAT=3}
。
或者对食材按照类型分,然后选出卡路里最高的食物:
1 | Map<Dish.Type, Optional<Dish>> map = list.stream().collect(groupingBy( |
输出结果:{OTHER=Optional[pizza], MEAT=Optional[pork], FISH=Optional[salmon]}
。如果不希望输出结果包含Optional,可以使用Collectors.collectingAndThen
方法:
1 | Map<Dish.Type, Dish> map = list.stream().collect(groupingBy( |
输出结果:{OTHER=pizza, FISH=salmon, MEAT=pork}
。
常与Collectors.groupingBy
组合使用的方法还有Collectors.mapping
。Collectors.mapping
方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来,比如对食材按照类型分类,然后输出各种类型食材下卡路里等级情况:
1 | Map<Dish.Type, HashSet<CaloricLevel>> map = list.stream().collect(groupingBy( |
Collectors.toCollection
方法可以方便的构造各种类型的集合。输出结果:{FISH=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], OTHER=[DIET, NORMAL]}
。
分区
分区类似于分组,只不过分区最多两种结果。Collectors.partitioningBy
方法用于分区操作,接收一个Predicate<T>
类型的Lambda表达式作为参数。比如将食材按照素食与否分类:
1 | Map<Boolean, List<Dish>> map = list.stream().collect(partitioningBy(Dish::isVegetarian)); |
输出结果:{false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
。
Collectors.partitioningBy
方法还支持传入分组函数或者其他规约操作,比如将食材按照素食与否分类,然后按照食材类型进行分类:
1 | Map<Boolean, Map<Dish.Type, List<Dish>>> map = list.stream().collect( |
输出结果:{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}, true={OTHER=[french fries, rice, season fruit, pizza]}}
。
再如将食材按照素食与否分类,然后筛选出各自类型中卡路里含量最低的食材:
1 | Map<Boolean, Dish> map = list.stream().collect( |
输出结果:{false=prawns, true=season fruit}
。
《Java 8实战》读书笔记