java8——收集器
收集器
collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中。Collectors 类包含了大量常用收集器的工厂方法,toList() 和 toSet() 就是其中最常见的两个,除了它们还有很多收集器,用来对数据进行对复杂的转换。
收集器的使用
收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个规约操作(由Collector来参数化)。
准备工作:创建一个Dish.java类,源码如下
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
public enum Type { MEAT, FISH, OTHER }
@Override
public String toString() {
return name;
}
public static final List<Dish> menu =
Arrays.asList( new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 400, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH));
}
接下来看一下收集器的操作
归约和汇总
归约:
long howManyDishes = menu.stream().collect(Collectors.counting());
long howManyDishes = menu.stream().count();
查找流中的最大值和最小值
你可以使用两个收集器: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方法后即执行我们需要的汇总操作。如:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
Collectors.summingLong 和 Collectors.summingDouble方法的作用完全一样,可以用于求和字段为long或double的情况。但汇总不仅仅是求和;还有Collectors.averagingInt, 联通对应的averagingLong和averagingDouble可以计算数值的平均数 :
double avgCalories =menu.stream().collect(averagingInt(Dish::getCalories));
很多时候你可能想要得到两个或者更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器
java.util.IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
IntSummaryStatistics能够获取接受最大值、最小值、总热量以及数量4个属性,同时计算平均值,你可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值。
这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值(getter)方法来访问结果。
连接字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
String shortMenu = menu.stream().map(Dish::getName).collect(joining())//joining在内部使用了StringBuilder来把生成的字符串逐个追加起来
joining工厂方法还有一个重载版本可以接受元素之间的分解符,这样你就可以得到一个逗号分隔的菜肴名称列表
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
Collectors.reducing工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。(但是,请记得方便程序员和可读性是头等大事!)例如,可以用reducing方法创建的收集器来计算你菜单的总热量,如下所示:
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
它需要三个参数。
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
- 第二个参数就是你在6.2.2节中使用的函数,将菜肴转换成一个表示其所含热量的int。
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
同样,你可以使用下面这样单参数形式的reducing来找到热量最高的菜,如下所示:
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反, collect方法的设计就是要改变容器,从而累积要输出的结果。
进一步简化reducing收集器的求和例子——引用Integer类的sum方法,而不用去写一个表达同一操作的Lambda表达式。如:
int totalCalories = menu.stream().collect(reducing(0,Dish::getCalories,Integer::sum));
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
请注意,就像流的任何单参数reduce操作一样,reduce(Integer::sum)返回的不是int而是Optional,以便在空留的情况下安全地执行规约操作,然后你只需要用Optinal对象中的get方法来提取李曼的值就行了。
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
分组
Collectors.groupingBy方法
Map<Dish.Type, List<Dish>> dishesByType=menu.stream().collect(groupingBy(Dish::getType));
当然我们还能实现多级分组
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; })
) );
Collectors.collectingAndThen方法
因为分组操作的Map结果中的每个值上包装的Optional没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,可以使用Collectors.collectingAndThen方法返回的收集器,使用如下:
Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, collectingAndThe(maxBy(comparingInt(Dish::getCalories)),Optional::get)));
分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数。它称分区函数。分区函数返回一个布尔值,这意味着得到的分组map的键类型是Boolean,于是它最多可以分为两组,例如
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
返回{false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
那么通过Map中键为true的值,就可以找出所有的素食菜肴了:
List<Dish> vegetarianDishes = partitionedMenu.get(true);
分区的好处在于保留了分区函数返回true或false的两套流元素列表。在上一个例子中,要得到非素食Dish的List,你可以使用两个筛选操作来访问partitionedMenu这个Map中false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的, partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
groupingBy(Dish::getType)));