重用一下之前的例子:包含一张佳肴列表的菜单。
就像刚刚看到的,在需要将流项目重组成集合时,一般会使用收集器(Stream
方法collect
的参数)。再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。这个结果可以是任何类型,可以复杂如代表一棵树的多级映射,或是简单如一个整数——也许代表了菜单的热量总和。
先来举一个简单的例子,利用counting
工厂方法返回的收集器,数一数菜单里有多少种菜:
long howManyDishes = menu.stream().collect(Collectors.counting();
这还可以写得更为直接:
long howManyDishes = menu.stream().count();
counting
收集器在和其他收集器联合使用的时候特别有用。后面的部分,假定已导入了Collectors
类的所有静态工厂方法:
import static java.util.stream.Collectors.*;
这样就可以写counting()
而用不着写Collectors.counting()
之类的了。继续探讨简单的预定义收集器,看看如何找到流中的最大值和最小值。
假设想要找出菜单中热量最高的菜。可以使用两个收集器,Collectors.maxBy
和Collectors.minBy
,来计算流中的最大或最小值。这两个收集器接收一个Comparator
参数来比较流中的元素。可以创建一个Comparator
来根据所含热量对菜肴进行比较,并把它传递给Collectors.maxBy
:
Comparator<Dish> dishcaloriesComparator = Comparator.comparingInt(Dish::getcalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishcaloriesComparator));
optional
是怎么回事。要回答这个问题,需要问“要是menu
为空怎么办”。那就没有要返回的菜了。Java 8
引入了optional
,它是一个容器,可以包含也可以不包含值。这里它完美地代表了可能也可能不返回菜肴的情况。在讲findAny
方法的时候简要提到过它。
另一个常见的返回单个值的归约操作是对流中对象的一个数值字段求和。或者可能想要求平均数。这种操作被称为汇总操作。让我们来看看如何使用收集器来表达汇总操作。
Collectors
类专门为汇总提供了一个工厂方法:Collectors.summingInt
。它可接受一个把对象映射为求和所需int
的函数,并返回一个收集器;该收集器在传递给普通的collect
方法后即执行需要的汇总操作。
举个例子来说,可以这样求出菜单列表的总热量:
int totalCalories = menu.stream().collect(summingInt(Dish::getcalories));
这里的收集过程如下图所示。在遍历流时,会把每一道菜都映射为其热量,然后把这个数字累加到一个累加器(这里的初始值0)。
Collectors.summingLong
和Collectors.summingDouble
方法的作用完全一样,可以用于求和字段为long
或double
的情况。
但汇总不仅仅是求和;还有Collectors.averagingInt
,连同对应的averagingLong
和averagingDouble
可以计算数值的平均数:
double avgCalories = menu.stream().collect(averagingInt(Dish::getcalories));
到目前为止,已经看到了如何使用收集器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing
操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getcalories));
这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics
的类里,它提供了方便的取值(getter
)方法来访问结果。打印menuStatisticobject
会得到以下输出:
IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
同样,相应的summarizingLong
和summarizingDouble
工厂方法有相关的LongSummaryStatistics
和DoubleSummaryStatistics
类型,适用于收集的属性是原始类型long
或double
的情况。
joining
工厂方法返回的收集器会把对流中每一个对象应用toString
方法得到的所有字符串连接成一个字符串。这意味着把菜单中所有菜肴的名称连接起来,如下所示:
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
请注意,joining
在内部使用了StringBuilder
来把生成的字符串逐个追加起来。此外还要注意,如果Dish
类有一个toString
方法来返回菜肴的名称,那无需用提取每一道菜名称的函数来对原流做映射就能够得到相同的结果:
String shortMenu = menu.stream().collect(joining());
二者均可产生以下字符串:
porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
但该字符串的可读性并不好。幸好,joining
工厂方法有一个重载版本可以接受元素之间的分界符,这样就可以得到一个逗号分隔的菜肴名称列表:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(","));
正如我们预期的那样,它会生成:
pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
到目前为止,已经探讨了各种将流归约到一个值的收集器。下面会展示为什么所有这种形式的归约过程,其实都是Collectors.reducing
工厂方法提供的更广义归约收集器的特殊情况。
事实上,已经讨论的所有收集器,都是一个可以用reducing
工厂方法定义的归约过程的特殊情况而已。Collectors.reducing
工厂方法是所有这些特殊情况的一般化。例如,可以用reducing
方法创建的收集器来计算菜单的总热量,如下所示:
int totalcalories = menu.stream().collect(reducing(0, Dish::getcalories, (i, j) -> i + j));
它需要三个参数。
0
是一个合适的值。int
。Binaryoperator
,将两个项目累积成一个同类型的值。这里它就是对两个int
求和。同样,可以使用下面这样单参数形式的reducing
来找到热量最高的菜,如下所示:
Optional<Dish> mostcalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getcalories() > d2.getcalories() ? d1 : d2));
你可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的
第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点;正
如我们在6.2.1节中所解释的,它将因此而返回一个optional对象。
你还可以进一步简化前面使用reducing收集器的求和例子——引用Integer类的sum方法,而不用去写一个表达同一操作的Lambda表达式。这会得到以下程序:
在现实中,counting
收集器也是类似地利用三参数reducing
工厂方法实现的。它把流中的每个元素都转换成一个值为1
的Long
型对象,然后再把它们相加:
public static <T> Collector<T,?,Long> counting(){
return reducing(OL, e -> 1L, Long::sum);
}
还有另一种方法不使用收集器也能执行相同操作——将菜肴流映射为每一道菜的热量,然后用前一个版本中使用的方法引用来归约得到的流:
int totalcalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
请注意,就像流的任何单参数reduce
操作一样,reduce(Integer::sum)
返回的不是int
而是optional
,以便在空流的情况下安全地执行归约操作。然后只需用optional
对象中的get
方法来提取里面的值就行了。请注意,在这种情况下使用get
方法是安全的,只是因为已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如orElse
或orElseGet
来解开optional
中包含的值更为安全。最后,更简洁的方法是把流映射到一个IntStream
,然后调用sum
方法,也可以得到相同的结果:
int totalcalories = menu.stream().mapToInt(Dish::getCalories).sum();
这再次说明了,函数式编程(特别是Java 8
的Collections
框架中加入的基于函数式风格原理设计的新API
)通常提供了多种方法来执行同一个操作。这个例子还说明,收集器在某种程度上比Stream
接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。
尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。无论是从可读性还是性能上看,这一般都是最好的决定。例如,要计菜单的总热量,更倾向于最后一个解决方案(使用IntStream)
,因为它最简明,也很可能最易读。同时,它也是性能最好的一个,因为IntStream
可以避免自动拆箱操作,也就是从Integer
到int
的隐式转换,它在这里毫无用处。