用流收集数据
Collection
、Collector
和Collect
的区别:
-
Collection
:Collection
是集合类接口,List
,Set
,Map
是它的子接口,这几个接口及他们的实现类在流操作中很常用。 -
Collector
:就是收集器,也是一个接口。它的工具类Collectors
提供了很多工厂方法(例如groupingBy
)创建的收集器.三大主要功能:将流元素归约和汇总为一个值,元素分组,元素分区。 -
collect
:collect
是一个终端操作(归约操作,就像reduce
一样可以接受各种作法作为参数,将流中的元素累积成一个汇总结果),它接受一个收集器作为参数。
- 查找流中的最大值与最小值
可以使用一个收集器,Collector.maxBy
来计算流中的最大值,这个收集器接收一个Comparator参数来比较流中的元素。Optional<Dish>
是考虑到返回值为空时情况。
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
//或者
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(comparingInt(Dish::getCalories)));
-
汇总:即对流中对象的一个数值字段求和
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
方法,那你无需使用Dish::getName
返回名称来对原流做映射就能够得到相同的结果:String shortMenu = menu.stream().collect(joining()); //若重写toString()方法,此时结果相同
joining
工厂方法有一个重载版本可以接受元素之间的分界符,这样你就可以得到一个逗号分隔的字符串。String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
以上的所有收集器,都是一个可以用reducing
工厂方法定义的归约过程的特殊情况。Collectors.reducing
工厂方法是所有这些特殊情况的一般化。
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
reduce方法需要三个参数:
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
- 第二个参数就是转换函数,转换为
int
类型 -
数是一个
BinaryOperator<T>
,也就是一个BiFunction<T,T,T>
,这就意味着他需要的函数必须接收两个参数,然后返回一个相同类型的值,将两个项目累积成一个同类型的值。
同样也有单参数reduceing
工厂方法创建的容器收集器看作是三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing
收集器传递给空流的collect
方法,收集器就没有起点;因此返回一个Optional
对象。
例如:
Optional<Dish> mostCalorieDish =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
但是也可以不使用收集器也能执行相同操作,使用map
映射,然后利用归约得到:
int totalCalories =
menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
就像流的任何单参数reduce
操作一样,reduce(Integer::sum)
返回的不是int
而是Optional<Integer>
,以便在空流的情况下安全地执行归约操作。然后只需用Optional
对象中的get
方法来提取里面的值就行了。在这种情况下使用get
方法是安全的,只是因为已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如orElse
或orElseGet
来解开Optional
中包含的值更为安全。更简单的方法就是映射到一个IntStream,然后调用sum方法。即:
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
可以看出来,其实收集器在某种程度上比Stream接口上直接提供的方法用起来更加复杂,但是使用收集器的好处在于能更容易重用和自定义。
-
分组
分组操作的结果是一个Map
,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。多级分组:
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; } ) ) );
返回的结果是一个两级 Map
:
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={DIET=[prawns], NORMAL=[salmon]},
OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}
相当于一个二维表格:
image.png
可以把第二个groupingBy
收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy
的第二个收集器可以是任何类型,而不一定是另一个groupingBy
,可以是counting()
或maxBy()
等,例如
(实际上普通的单参数groupingBy(f)
实际上是groupingBy(f, toList())
的简单写法)
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
{MEAT=3, FISH=2, OTHER=4} //结果
-
把收集器的结果转换为另一种类型,可以使
Collectors.collectingAndThen
工厂方法返回的收集器例如:
Map<Dish.Type, Dish> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType,//分类函数 collectingAndThen( maxBy(comparingInt(Dish::getCalories)),//包装后的收集器 Optional<T> Optional::get))); //转换函数 //结果 {FISH=salmon, OTHER=pizza, MEAT=pork}
这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。
collect
操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用maxBy
建立的那个,而转换函数Optional::get
则把返回的Optional
中的值提取出来 -
与groupingBy联合使用的其他收集器---
mapping
这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(
dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT; },
toCollection(HashSet::new) )));
//结果
{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
-
分区
partitioningBy
分区是分组的特殊情况,分区函数返回一个布尔值,也就是说分区得到的分组Map
的键类型是Boolean
,它最多可以分为两组——true
是一组,false
是一组。(注意分区返回键的类型是Boolea
)
例如://通过分区实现: Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian)); //通过分组也能够实现: Map<Boolean, List<Dish>> partitionedMenu= menu.stream().collect(groupingBy(Dish::isVegetarian)); //结果相同,所以说分区是分组的一种特殊情况 {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]} //通过map.get(true/false)即可得到相应的结果
Collectors类
的静态工厂方法能够创建的所有收集器:(注意要配合终端方法collect()
使用)
工厂方法 | 返回类型 | 用处 |
---|---|---|
toList |
List<T> |
把流中所有项目收集到一个 List
|
toSet |
Set<T> |
把流中所有项目收集到一个 Set ,删除重复项 |
toCollection |
Collection<T> |
把流中所有项目收集到给定的供应源创建的集合 |
counting |
Long |
计算流中元素的个数 |
summingInt |
Integer |
对流中项目的一个整数属性求和 |
averagingInt |
Double |
计算流中项目 Integer 属性的平均值 |
summarizingInt |
IntSummaryStatistics |
收集关于流中项目Integer 属性的统计值,例如最大、最小、总和与平均值 |
joining |
String |
连接对流中每个项目调用 toString 方法所生成的字符串 |
maxBy |
Optional<T> |
一个包裹了流中按照给定比较器选出的最大元素的 Optional ,或如果流为空则为 Optional.empty()
|
minBy |
Optional<T> |
一个包裹了流中按照给定比较器选出的最小元素的 Optional ,或如果流为空则为 Optional.empty()
|
reducing |
归约操作产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值 |
collectingAndThen |
转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 |
groupingBy |
Map<K, List<T>> |
根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键 |
partitioningBy |
Map<Boolean,List<T>> |
根据对流中每个项目应用谓词的结果来对项目进行分区 |
-
Collector
Collector
接口定义:public interface Collector <T,A,R>{ Supplier<A> supplier(); BiConsumer <A,T>accumulator(); Function <A,R>finisher(); BinaryOperator<A> combiner(); Set <characteristics>characteristics(); } /** * 前四个方法都会返回一个会被collect方法调用的函数 * 第五个方法则提供了一系列特征,就是告诉collect方法在执行规约操作时可以应用哪些优化,比如并行化 */
- T是流中要收集的项目的泛型。
- A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
- R是收集操作得到的对象的类型。
-
建立新的结果容器:
supplier
方法
supplier
方法必须返回一个结果为空的Supplier
,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。public Supplier<List<T>> supplier() { return () -> new ArrayList<T>(); } //或 public Supplier<List<T>> supplier() { return ArrayList::new; }
-
将元素添加到结果容器:
accumulator
方法
accumulator
方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第n个元素本身。该函数将返回void
,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。public BiConsumer<List<T>, T> accumulator() { return (list, item) -> list.add(item); } //或 public BiConsumer<List<T>, T> accumulator() { return List::add; }
-
对结果容器应用最终转换:
finisher
方法
在遍历完流后,finisher
方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher
方法只需返回identity
函数:public Function<List<T>, List<T>> finisher() { return Function.identity(); }
-
合并两个结果容器:
combiner
方法
combiner
方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并,这样,就可以做到对流进行并行归约了//???public BinaryOperator<List<T>> combiner() { return (list1, list2) -> { list1.addAll(list2); return list1; } }
-
characteristics
方法
返回一个不可变的Characteristics
集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics
是一个包含三个项目的枚举:-
UNORDERED
——归约结果不受流中项目的遍历和累积顺序的影响。 -
CONCURRENT
——accumulato
r函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED
,那它仅在用于无序数据源时才可以并行归约。 -
IDENTITY_FINISH
——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
-
-
自定义收集器:
//6.6