文章是个人阅读《Java8实战》过程中的重点摘抄,可能晦涩,没有示例代码,后续会补充总结完善。
本章内容
- 用Collectors类创建和使用收集器
- 将数据流归约为一个值
- 汇总:归约的特殊情况
- 数据分组和分区
- 开发自己的自定义收集器
核心问题
1.收集器的作用是什么?核心接口是什么?2.Collectors常用静态方法的使用。
3.对比reduce和collect两个接口的区别。
4.分组和分区操作相关的方法
5.自定义收集器
概述
collect是一个规约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结。具体的做法是通过定义一个新的Collector接口,因此区分Collection、Collector、Collect十分重要。
6.1 收集器简介
函数式编程相对于指令式编程的一个主要优势:只需要指出希望的结果(做什么),而不用操心执行的步骤(如何做)。
6.1.1 收集器作用与高级归约
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
收集器非常有用,因为他可以简单灵活的定义collect用来生成结果集合的标准。对流调用collect方法,将出发流的归约操作(由Collector来参数化)。
Collector会对元素应用一个转换函数,并将结果累积在一个数据结构中,从而产生最终输出。Collector接口方法的实现决定了如何对流执行归约操作。
6.2.1 预定义收集器
在日常的使用过程中,我们使用Collectors类的静态工厂方法就可以完成大部分工作。这些方法可以方便的创建常用收集器的实例,例如toList、groupBy等。
他主要提供三大功能
- 将元素归约和汇总为一个值
- 元素的分组
- 元素的分区
6.2 归约和汇总
就像我们前面看到的那样,将流重组为集合时需要使用收集器,更宽泛的讲就是将,但凡要将流中的元素合并成一个结果时就可以用收集器。这个结果可以是多种类型,复杂点的集合,或简单点的一个整数,这个结果的返回由Collector接口的实现来决定。
注意:了解Collectors中静态方法很重要,只有了解了有哪些方法,才能灵活应用。下面将介绍Collectors部分常用方法
6.2.1 查找流中的最大值最小值:maxBy、minBy
这两个收集器接受Collector参数来比较流中的元素。
// 创建一个Collector来根据食物热量对菜肴进行比较
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
6.2.2 汇总summingInt、averagingInt、summarizingInt
汇总是一个常见的操作,例如对流对象的一个字段值求和或者平均数。常用操作:
- summingInt 求和
- averagingInt 求平均数
- summarizingInt 复合操作,一次性求出总数、最大值、最小值、总和、平均值
summingInt接受一个将对象映射为int的函数,并返回一个收集器。
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
// IntSummaryStatistics打印结果
IntSummaryStatistics{count=9, sum=4300, min=120,average=477.777778, max=800}
在以上介绍的汇总方法中,都有想用的long和double版本。
6.2.3 连接字符串:joining
joining方法会对流中元素应用toString方法,得到的所有字符链接为一个以空格为分隔符的字符串。joining有一个重载方法,可以接受分隔符。
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
// 如果Dish类提供了toString方法,返回菜名,可以简写
String shortMenu = menu.stream().collect(joining());
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
6.2.4 广义的归约汇总:reducing
到此为止,我们已经讨论的所有收集器,都是reducing工厂方法定义的归约的特殊情况。reducing是所有特殊情况的一般化。他需要三个参数
- 1.归约操作的起始值
- 2.一个转换函数
- 3.一个BinaryOperator的类型参数
同时,reducing还有两个重载方法,一个是只接受BinaryOperator的方法;一个是接受初始值和BinaryOperator的方法。
// 求菜卡路里总和
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
// 求热量最高的菜
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
归约与收集
Stream接口的reduce和collect都可以做归约操作,两种方法通常会获取相同的结果,那他们之前有什么不同呢?例如,我们可以使用reduce的方法来实现collect的toList
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();
List<Integer> numbers = stream.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; });
这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于, reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反, collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代代码片段是在乱用reduce方法,因为它在原地改变了作为累加器的List。
以错误的语义使用reduce方法还会造成一个实际问题:这个累积过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响到性能。这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作。
根据情况选择最佳方案
函数式编程通常提供了多中方法来执行同一操作。例如统计菜肴的卡路里
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
我们的建议是:尽可能为手头的问题探索不用的解决方案,在通用的解决方案里面,始终选择最专门化化的一个。从可读性和性能上讲,这一般都是最好的决定。
6.3 分组:groupingBy
一个常见的应用场景是对集合进行分组,如果我们使用指令式风格来实现,操作很啰嗦;我们使用函数式风格来写,就会简短易懂。
例如,把菜单中的菜按照类型进行分类
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
// 结果
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],MEAT=[pork, beef, chicken]}
这里给groupingBy方法传递了一个Function(以方法引用的形式),他提取了每一道Dish的Type。我们把这个Function叫做分类函数,因为他用来把流中的元素分成组。但是分类函数不一定式方法引用,因为分类条件可能更复。例如:按照热量划分。
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
));
6.3.1 多级分组
多级分组我们可以使用groupingBy的重载方法,除了接受一个参数外,还接受Collector类型的第二个参数。如果进行二级分组的话,我们可以把一个内层groupingBy传递给外层的groupingBy,并定义一个为流中项目分类的二级标准。
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> 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;
} )
)
);
// 结果
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={DIET=[prawns], NORMAL=[salmon]},
OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}
6.3.2 按子组收集数据
// 按照每类菜的数量分组
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
// 按照每类菜的最高热量分组
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));
注意:带有一个参数的groupingBy(f)实际上是groupingBy(f,toList())的简写。
把收集器的结果转换为两一种类型:groupinBy的重载
像上例中,我们在按照子组收集数据的时候,返回的结果是Map结果中的每个值包装的Optional没有实际用处。我们像去掉,就需要将收集器返回的结果转换为另一种类型,可以使用Colectors.collectingAndThen工厂方法返回收集器。
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream().
collect(groupingBy(
Dish::getType,
collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
这个工厂方法接受两个参数——要转换的收集器和转换函数,返回另一个收集器。这个收集器相当于旧收集器的包装,collect操作的最后一部就是将返回值通过函数转换成一个映射。在这里背包起来的收集器就是maxBy建立的那个,转换函数Optional::get则把返回的Optinal中的值提取出来。
示例解析
与groupingBy联合使用的其他收集器:mapping()
》》》
6.4 分区:partitioningBy
分区是分组的特殊情况:由一个谓词(返回boolean值)作为分类函数,称为分区函数。因此得到的分组Map的key是true和false,分为两组。
// 按照素食和素食分组
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
// 返回结果
{false=[pork, beef,
6.4.1 分区的优势
分区的好处在于保留了分区函数返回true或false的两套元素列表。
partitioningBy方法还有一个重载版本,第二个参数接受收集器,实现多级分组。
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(partitioningBy(Dish::isVegetarian,groupingBy(Dish::getType)));
// 结果
{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
true={OTHER=[french fries, rice, season fruit, pizza]}}
Collectors类的静态工厂方法
6.5 收集器接口
》》》
6.6 开发收集器
》》》
6.7 小节
- collect是一个终端操作,他接受的参数是将流中的元素累积到汇总结果的各种方式。
- API中收集器包括很多常用的功能
- 将流元素归约和汇总到一个值,例如计算最大值、最小值等
- 可以分组和分区,并且将收集器符合起来,实现多级分组分区或归约
- 通过分析Collector接口,可以实现接口方法来开发自己的收集器 》》》