Java8-Collect收集Stream
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接口来定义的。
预定义的收集器
下面简单演示基本的内置收集器。模拟数据源如下:
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)
);
最大值,最小值,平均值
// 为啥返回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的元素拼起来?
//直接连接
String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining());
//逗号
String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));
toList
List<String> names = dishes.stream().map(Dish::getName).collect(toList());
将原来的Stream映射为一个单元素流,然后收集为List。
toSet
Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());
将Type收集为一个set,可以去重复。
toMap
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创建收集器。比如,求和
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
Optional<Integer> totalCalories3 = dishes.stream().map(Dish::getCalories).reduce(Integer::sum);
虽然都可以,但考量效率的话,还是要选择下面这种
int sum = dishes.stream().mapToInt(Dish::getCalories).sum();
根据情况选择最佳方案
上面的demo说明,函数式编程通常提供了多种方法来执行同一个操作,使用收集器collect比直接使用stream的api用起来更加复杂,好处是collect能提供更高水平的抽象和概括,也更容易重用和自定义。
我们的建议是,尽可能为手头的问题探索不同的解决方案,始终选择最专业的一个,无论从可读性还是性能来看,这一般都是最好的决定。
reducing除了接收一个初始值,还可以把第一项当作初始值
Optional<Dish> mostCalorieDish = dishes.stream()
.collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
reducing
关于reducing的用法比较复杂,目标在于把两个值合并成一个值。
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里的第一个参数当做初始值。
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效果:
//手动实现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;
}
);
关于上述做法解释一下。
<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。
Map<Type, List<Dish>> dishesByType = dishes.stream().collect(groupingBy(Dish::getType));
这里
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,那么可以自定义分类器为:
private CaloricLevel getCaloricLevel(Dish d) {
if (d.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (d.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}
再传入参数即可
Map<CaloricLevel, List<Dish>> dishesByLevel = dishes.stream()
.collect(groupingBy(this::getCaloricLevel));
多级分组
groupingBy还重载了其他几个方法,比如
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的源码为:
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。
//多级分组
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);
});
});
验证结果为:
----------------------------------
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
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
Map<Type, Optional<Dish>> mostCaloricByType = dishes.stream()
.collect(groupingBy(Dish::getType, maxBy(Comparator.comparingInt(Dish::getCalories))));
这里的Optional没有意义,因为肯定不是null。那么只好取出来了。使用collectingAndThen
Map<Type, Dish> mostCaloricByType = dishes.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
到这里似乎结果出来了,但IDEA不同意,编译黄色报警,按提示修改后变为:
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则直接抛出异常:
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
Map<Type, Integer> totalCaloriesByType = dishes.stream()
.collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接收特定类型元素的收集器适应不同类型的对象。我么来看一个使用这个收集器的实际例子。比如你想得到,对于每种类型的Dish,菜单中都有哪些CaloricLevel。我们可以把groupingBy和mapping收集器结合起来,如下所示:
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. 例如,如果你是素食者,你可能想要把菜单按照素食和非素食分开:
Map<Boolean, List<Dish>> partitionedMenu = dishes.stream().collect(partitioningBy(Dish::isVegetarian));
当然,使用filter可以达到同样的效果:
List<Dish> vegetarianDishes = dishes.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
分区相对来说,优势就是保存了两个副本,当你想要对一个list分类时挺有用的。同时,和groupingBy一样,partitioningBy一样有重载方法,可以指定分组value的类型。
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收集器的最后一个例子,我们把菜单数据模型放在一边,来看一个更加复杂也更为有趣的例子:将数组分为质数和非质数。
首先,定义个质数分区函数:
private boolean isPrime(int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}
然后找出1到100的质数和非质数
Map<Boolean, List<Integer>> partitionPrimes = IntStream.rangeClosed(2, 100).boxed()
.collect(partitioningBy(this::isPrime));
参考
- Java8 In Action
Java8-Collect收集Stream的更多相关文章
- Java8学习(4)-Stream流
Stream和Collection的区别是什么 流和集合的区别是什么? 粗略地说, 集合和流之间的差异就在于什么时候进行计算.集合是一个内存中的数据结构,它包含数据结构中目前所有的值--集合中的每个元 ...
- 这可能是史上最好的 Java8 新特性 Stream 流教程
本文翻译自 https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ 作者: @Winterbe 欢迎关注个人微信公众 ...
- Java8 新特性 Stream 非短路终端操作
非短路终端操作 Java8 新特性 Stream 练习实例 非短路终端操作,就是所有的元素都遍厉完,直到最后才结束.用来收集成自己想要的数据. 方法有: 遍厉 forEach 归约 reduce 最大 ...
- Java8 新特性 Stream 无状态中间操作
无状态中间操作 Java8 新特性 Stream 练习实例 中间无状态操作,可以在单个对单个的数据进行处理.比如:filter(过滤)一个元素的时候,也可以判断,比如map(映射)... 过滤 fil ...
- Java8 新特性 Stream() API
新特性里面为什么要加入流Steam() 集合是Java中使用最多的API,几乎每一个Java程序都会制造和处理集合.集合对于很多程序都是必须的,但是如果一个集合进行,分组,排序,筛选,过滤...这些操 ...
- 【Java8新特性】面试官:谈谈Java8中的Stream API有哪些终止操作?
写在前面 如果你出去面试,面试官问了你关于Java8 Stream API的一些问题,比如:Java8中创建Stream流有哪几种方式?(可以参见:<[Java8新特性]面试官问我:Java8中 ...
- Java8中的Stream流式操作 - 入门篇
作者:汤圆 个人博客:javalover.cc 前言 之前总是朋友朋友的叫,感觉有套近乎的嫌疑,所以后面还是给大家改个称呼吧 因为大家是来看东西的,所以暂且叫做官人吧(灵感来自于民间流传的四大名著之一 ...
- Java8 如何进行stream reduce,collection操作
Java8 如何进行stream reduce,collection操作 2014-07-16 16:42 佚名 oschina 字号:T | T 在java8 JDK包含许多聚合操作(如平均值,总和 ...
- java8新特性--Stream的基本介绍和使用
什么是Stream? Stream是一个来自数据源的元素队列并可以进行聚合操作. 数据源:流的来源. 可以是集合,数组,I/O channel, 产生器generator 等 聚合操作:类似SQL语句 ...
- Java8新特性——stream流
一.基本API初探 package java8.stream; import java.util.Arrays; import java.util.IntSummaryStatistics; impo ...
随机推荐
- C#/AutoCAD 2018/ObjectArx/二次开发添加图形对象步骤和添加直线的例子(三)
1.创建一个图形对象的步骤如下(1)得到创建对象的图形数据库:(2)在内存中创建实体类的一个对象:(3)定义一个指向当前数据库的事务处理:(4)打开图形数据库的块表:(5)打开一个存储实体的块表记录( ...
- 弹幕视频播放app案例分析
产品 哔哩哔哩动画 相对于其他视频播放软件来说,哔哩哔哩动画没有广告影响观看体验,而且内容更偏重于二次元,因此我更倾向于使用它. 第一部分 调研, 评测 #第一次上手体验 用起来还是比较方便,可以快速 ...
- 第201621123043 《Java程序设计》第14周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结与数据库相关内容. 2. 使用数据库技术改造你的系统 2.1 简述如何使用数据库技术改造你的系统.要建立什么表?截图你的表设计. 2 ...
- 【iOS】swift 保持代码优美的10个方法
这篇Swift风格指南与你看到的其他的指南有所不同,此篇指南主要焦点集中在打印和Web展示的可读写上.我们创建此篇风格指南的目的,是为了让我们的图书.教程以及初学者套件中的代码保持优美和一致,即使我们 ...
- 【转】optach学习
[转自:https://yq.aliyun.com/articles/28007,仅作学习用途] 摘要: Opatch 是oracle公司开发的安装,卸载,检测patch冲突的工具,管理oracle所 ...
- 剑指offer-数据流中的中位数
题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值. ...
- MySQL binlog 日志
一:MySQL 日志的三种类型: statement.row.mix 格式.推荐使用row格式. 怎么设置自己的日志格式呢? 1. set globle binlog_format='MIXED' 2 ...
- api-gateway实践(09)支持rest服务注册
一.GET-GET 1.前端定义 2.后端定义 2.1.基础定义 2.2.path参数.head参数.query参数 2.3.常量参数 2.4.系统参数 2.5.结果定义 二.POST-POST 1. ...
- Spring知识点回顾(01)Java Config
Spring知识点回顾(01) 一.Java Config 1.服务和服务注入 2.Java 注解 :功能更强一些 3.测试验证 二.注解注入 1.服务和服务注入 2.配置加载 3.测试验证 三.总结 ...
- Spring Security入门(3-5)Spring Security 的鉴权 - 决策管理器和投票器
1.决策管理器的运行原理: 2.Spring Security提供的决策管理器实现 3.用户自定义的决策管理器