一.收集器
收集器可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。
案例,对一个交易列表按货币分组。
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream().collect(groupingBy(Transaction::getCurrency));
收集器的功能可以从Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
二.归约和汇总
- 查找流中的最大值和最小值
Collectors.maxBy
和Collectors.minBy
,来计算流中的最大或最小值。
这两个收集器接收一个Comparator参数来比较流中的元素。
假设你想要找出菜单中热量最高的菜。
//以创建一个Comparator来根据所含热量对菜肴进行比较,并把它传递给Collectors.maxBy
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream() .collect(maxBy(dishCaloriesComparator));
- 汇总
Collectors类专门为汇总提供了一个工厂方法:
Collectors.summingInt
。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。
举个例子,样求出菜单中菜肴列表的总热量
//在遍历流时,会把每一道菜都映射为其热量,然后把这个数字累加到一个累加器(这里的初始值0)。
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500,"麻婆豆腐"));
menu.add(new Dish(400,"爆炒猪肝"));
menu.add(new Dish(400,"油焖茄子"));
menu.add(new Dish(400,"蒜蓉龙虾"));
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
//更简洁可以如下写法,上行代码介绍summingInt
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
Collectors.summingLong
和Collectors.summingDouble
方法的作用完全一样,可以用于求和字段为long或double的情况。
汇总不仅仅是求和;还有Collectors.averagingInt
,连同对应的averagingLong
和averagingDouble
可以计算数值的平均数:
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500,"麻婆豆腐"));
menu.add(new Dish(400,"爆炒猪肝"));
menu.add(new Dish(400,"油焖茄子"));
menu.add(new Dish(400,"蒜蓉龙虾"));
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
很多时候,可能想要得到两个或更多这样的结果,而且只需一次操作就可以完成。在这种情况下,你可以使用
summarizingInt
工厂方法返回的收集器。
例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500,"麻婆豆腐"));
menu.add(new Dish(400,"爆炒猪肝"));
menu.add(new Dish(400,"油焖茄子"));
menu.add(new Dish(400,"蒜蓉龙虾"));
IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println(menuStatistics);
这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值(getter)方法来访问结果。打印menuStatisticobject会得到以下输出:
IntSummaryStatistics{count=4, sum=1700, min=400, average=425.000000, max=500}
同样,相应的
summarizingLong
和summarizingDouble
工厂方法有相关的LongSummaryStatistics
和DoubleSummaryStatistics
类型,适用于收集的属性是原始类型long或double的情况。
- 连接字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
例如:
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500,"麻婆豆腐"));
menu.add(new Dish(400,"爆炒猪肝"));
menu.add(new Dish(400,"油焖茄子"));
menu.add(new Dish(400,"蒜蓉龙虾"));
String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining());
System.out.println(shortMenu);
结果:
麻婆豆腐爆炒猪肝油焖茄子蒜蓉龙虾
此结果可读性并不好。joining工厂方法有一个重载版本可以接受元素之间的分界符,就可以得到一个逗号分隔的列表:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
结果:
麻婆豆腐,爆炒猪肝,油焖茄子,蒜蓉龙虾
- 广义的归约汇总
事实上所有收集器,都是一个可以用
reducing
工厂方法定义的归约过程的特殊情况而已。Collectors.reducing
工厂方法是所有这些特殊情况的一般化。
用reducing方法创建的收集器来计算菜单的总热量,如下所示:
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500,"麻婆豆腐"));
menu.add(new Dish(400,"爆炒猪肝"));
menu.add(new Dish(400,"油焖茄子"));
menu.add(new Dish(400,"蒜蓉龙虾"));
int totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
System.out.println(totalCalories);
//简单写法:
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
它需要三个参数。
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
- 第二个参数就是Lambda的函数,将菜肴转换成一个表示其所含热量的int。
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示:
Optional<Dish> mostCalorieDish =
menu.stream().collect(Collectors.reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
//简化写法:
Optional<Dish> mostCalorieDish =
menu.stream().reduce((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2);
可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点。
三.分组
- 简单分组
数据库操作会根据一个或多个属性对集合中的项目进行分组。Java 8所推崇的函数式风格来重写的话,就很容易转化为一个非常容易看懂的语句。
用Collectors.groupingBy工厂方法返回的收集器,根据不同厨师分组菜品:
List<Dish> menu = new ArrayList<>();//菜单
menu.add(new Dish(500, "麻婆豆腐", "王师傅"));
menu.add(new Dish(400, "爆炒猪肝", "王师傅"));
menu.add(new Dish(400, "油焖茄子", "李师傅"));
menu.add(new Dish(400, "蒜蓉龙虾", "赵师傅"));
Map<String, List<Dish>> map = menu.stream().collect(Collectors.groupingBy(Dish::getCooker));
System.out.println(map);
这里给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每一道Dish的Dish.cooker。我们把这个Function叫作
分类函数
,因为它用来把流中的元素分成不同的组。分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。
- 多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。
假设想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。
public enum CaloricLevel { DIET, NORMAL, FAT }
并且对菜单中的菜肴按照厨师和热量进行分组:
Map<String, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
menu.stream().collect(
Collectors.groupingBy(Dish::getCooker,
Collectors.groupingBy(dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}})));
System.out.println(dishesByTypeCaloricLevel);
结果:
李师傅={FAT=[Dish(calories=800, name=油焖茄子, cooker=李师傅)]},
王师傅={NORMAL=[Dish(calories=600, name=麻婆豆腐, cooker=王师傅)], DIET=[Dish(calories=300, name=爆炒猪肝, cooker=王师傅)]},
赵师傅={NORMAL=[Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅)]}
这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map。一般来说,把groupingBy看作“桶”比较容易明白。第一个groupingBy给每个键建立了一个桶。然后再用下游的收集器去收集每个桶中的元素,以此得到n级分组。
- 按子组收集数据
我们可以通过多级分组把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。
例如,要数一数菜单中每个厨师下有多少菜品,可以传递counting收集器作为groupingBy收集器的第二个参数:
List<Dish> menu = new ArrayList<>();//菜单;
menu.add(new Dish(600, "麻婆豆腐", "王师傅"));
menu.add(new Dish(300, "爆炒猪肝", "王师傅"));
menu.add(new Dish(800, "油焖茄子", "李师傅"));
menu.add(new Dish(700, "蒜蓉龙虾", "赵师傅"));
Map<String, Long> typesCount = menu.stream()
.collect(Collectors.groupingBy(Dish::getCooker,
Collectors.counting()));
其结果是下面的Map:
{李师傅=1, 王师傅=2, 赵师傅=1}
再举一个例子,查找菜单中厨师烧制的菜品热量最高的菜肴的收集器改一改,按照菜的厨师分类:
Map<String, Optional<Dish>> mostCaloricByType = menu.stream()
.collect(Collectors.groupingBy(Dish::getCooker,
Collectors.maxBy(
Comparator.comparingInt(Dish::getCalories))));
System.out.println(mostCaloricByType);
其结果是下面的Map:
{
李师傅=Optional[Dish(calories=800, name=油焖茄子, cooker=李师傅)],
王师傅=Optional[Dish(calories=600, name=麻婆豆腐, cooker=王师傅)],
赵师傅=Optional[Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅)]
}
因为分组操作的Map结果中的每个值上包装的Optional没什么用,所以可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,可以使用
Collectors.collectingAndThen
工厂方法返回的收集器。
查找菜单中厨师烧制的菜品热量最高的菜肴的收集器改一改,按照菜的厨师分类:
Map<String, Dish> mostCaloricByType = menu.stream()
.collect(Collectors.groupingBy(Dish::getCooker,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
Optional::get)));
System.out.println(mostCaloricByType);
这个工厂方法接受两个参数:
要转换的收集器
以及转换函数
,并返回另一个收集器
。这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来。这个操作放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。
其结果是下面的Map:
{
李师傅=Dish(calories=800, name=油焖茄子, cooker=李师傅),
王师傅=Dish(calories=600, name=麻婆豆腐, cooker=王师傅),
赵师傅=Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅)
}
分析:
- 收集器用虚线表示,因此groupingBy是最外层,根据菜肴的类型把菜单流分组,得到三个子流。
- groupingBy收集器包裹着collectingAndThen收集器,因此分组操作得到的每个子流都用这第二个收集器做进一步归约。
- collectingAndThen收集器又包裹着第三个收集器maxBy。
- 随后由归约收集器进行子流的归约操作,然后包含它的collectingAndThen收集器会对其结果应用Optional:get转换函数。
- 对三个子流分别执行这一过程并转换而得到的三个值,也就是各个厨师手中热量最高的Dish,将成为groupingBy收集器返回的Map中与各个分类键(Dish的类型)相关联的值。
- [2] 与groupingBy联合使用的其他收集器的例子
通过groupingBy工厂方法的第二个参数传递的收集器将会对分到同一组中的所 有流元素执行进一步归约操作。
例如,求出所有菜肴热量总和的收集器,不过这次是对每个厨师的Dish求和:
Map<String, Integer> totalCaloriesByType =
menu.stream().collect(Collectors.groupingBy(Dish::getCooker,
Collectors.summingInt(Dish::getCalories)));
System.out.println(totalCaloriesByType);
常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两 个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加 之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型 的对象。
比方,对于每个厨师的Dish, 菜单中都有哪些CaloricLevel。
List<Dish> menu = new ArrayList<>();//菜单;
menu.add(new Dish(600, "麻婆豆腐", "王师傅"));
menu.add(new Dish(300, "爆炒猪肝", "王师傅"));
menu.add(new Dish(800, "油焖茄子", "李师傅"));
menu.add(new Dish(700, "蒜蓉龙虾", "赵师傅"));
Map<String, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
Collectors.groupingBy(Dish::getCooker, Collectors.mapping(
dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
},
Collectors.toSet())));
System.out.println(caloricLevelsByType);
传递给映射方法的转换函数将Dish映射成了它的 CaloricLevel:生成的CaloricLevel流传递给一个toSet收集器,它和toList类似,不过是 把流中的元素累积到一个Set而不是List中,以便仅保留各不相同的值。
结果:
{李师傅=[FAT], 王师傅=[NORMAL, DIET], 赵师傅=[NORMAL]}
在上 一个示例中,对于返回的Set是什么类型并没有任何保证。但通过使用
toCollection
,你就可以有更多的控制。
例如,你可以给它传递一个构造函数引用来要求HashSet:
Map<String, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
Collectors.groupingBy(Dish::getCooker, Collectors.mapping(
dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
},
Collectors.toCollection(HashSet::new))));
四.分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。
例如,把菜单按照素食和非素食分开:
@Data
@AllArgsConstructor
public class Dish {
private int calories;
private String name;
private String cooker;
private boolean isVegetarian;
}
List<Dish> menu = new ArrayList<>();//菜单;
menu.add(new Dish(600, "麻婆豆腐", "王师傅",true));
menu.add(new Dish(300, "爆炒猪肝", "王师傅",false));
menu.add(new Dish(800, "油焖茄子", "李师傅",true));
menu.add(new Dish(700, "蒜蓉龙虾", "赵师傅",false));
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println(partitionedMenu);
这会返回下面的Map:
{
false=[Dish(calories=300, name=爆炒猪肝, cooker=王师傅, isVegetarian=false),
Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅, isVegetarian=false)],
true=[Dish(calories=600, name=麻婆豆腐, cooker=王师傅, isVegetarian=true),
Dish(calories=800, name=油焖茄子, cooker=李师傅, isVegetarian=true)]
}
那么通过Map中键为true的值,就可以找出所有的素食菜肴了:
List<Dish> vegetarianDishes = partitionedMenu.get(true);
同样的分区谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果:
List<Dish> vegetarianDishes = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
分区的优势
分区的好处在于保留了分区函数返回true或false的两套流元素列表。
partitioningBy
工厂方法有一个重载版本,可以传递第二个收集器。
例如:
Map<Boolean, Map<String, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
Collectors.partitioningBy(Dish::isVegetarian,
Collectors.groupingBy(Dish::getCooker)));
System.out.println(vegetarianDishesByType);
结果:
{
false={王师傅=[Dish(calories=300, name=爆炒猪肝, cooker=王师傅, isVegetarian=false)],
赵师傅=[Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅, isVegetarian=false)]
},
true={李师傅=[Dish(calories=800, name=油焖茄子, cooker=李师傅, isVegetarian=true)],
王师傅=[Dish(calories=600, name=麻婆豆腐, cooker=王师傅, isVegetarian=true)]
}
}
同样,找到素食和非素 食中热量最高的菜:
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
Collectors.partitioningBy(Dish::isVegetarian,
Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparingInt(Dish::getCalories)),
Optional::get)));
System.out.println(mostCaloricPartitionedByVegetarian);
结果:
{false=Dish(calories=700, name=蒜蓉龙虾, cooker=赵师傅, isVegetarian=false),
true=Dish(calories=800, name=油焖茄子, cooker=李师傅, isVegetarian=true)}
五.总结
Collectors类的静态工厂方法
- toList 把流中所有对象收集到一个List
使用示例:List dishes = menuStream.collect(toList());
- toSet 把流中所有对象收集到一个Set,删除重复项
使用示例:Set dishes = menuStream.collect(toSet());
- toCollection 把流中所有对象收集到给定的供应源创建的集合
使用示例:Collection dishes = menuStream.collect(toCollection(), ArrayList::new);
- counting 计算流中元素的个数
使用示例:long howManyDishes = menuStream.collect(counting());
- summingInt 对流中对象的一个整数属性求和
使用示例:int totalCalories = menuStream.collect(summingInt(Dish::getCalories));
- averagingInt 计算流中对象Integer属性的平均值
使用示例:double avgCalories = menuStream.collect(averagingInt(Dish::getCalories));
- summarizingInt 收集关于流中对象 Integer 属性的统计值,例如最大、最小、总和与平均值
使用示例:IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories));
- joining 连接对流中每个对象调用toString方法所生成的字符串
使用示例:String shortMenu = menuStream.map(Dish::getName).collect(joining(", "));
- maxBy 一个包裹了流中按照给定比较器选出的最大元素的 Optional, 或如果流为空则为 Optional.empty()
使用示例:Optional fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
- minBy 一个包裹了流中按照给定比较器选出的最小元素的 Optional, 或如果流为空则为 Optional.empty()
使用示例:Optional lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories)));
- reducing 从一个作为累加器的初始值开始,利用 BinaryOperator 与流 中的元素逐个结合,从而将流归约为单个值
使用示例:int totalCalories = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum));
- collectingAndThen 包裹另一个收集器,对其结果应用转换函数
使用示例:int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size));
- groupingBy 根据对象的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键
使用示例:Map<String,List> dishesByCooker = menuStream.collect(groupingBy(Dish::getCooker));
- partitioningBy 根据对流中每个对象应用谓词的结果来对项目进行分区
使用示例:Map<Boolean,List> vegetarianDishes = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
over