6.函数式数据处理-用流收集数据

文章是个人阅读《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接口,可以实现接口方法来开发自己的收集器 》》》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值