1. 收集器简介
收集器就是流的终端操作操作类,常用的就是collect
操作(当然还有其他的),这个操作的参数就是一个收集器,他有很多官方定义好的用法,下文会一一介绍。
collect
操作符一般都接受Collectors
中定义的归约方法:
List<Transaction> transactions =
transactionStream.collect(Collectors.toList());
2. 归约和汇总
归约为一个整数——流中所有元素个数:
long howManyDishes = menu.stream().collect(Collectors.counting());
还可以更简洁:
long howManyDishes = menu.stream().count();
查找流中最小值和最大值
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
汇总
对流中对象中的某个字段求和的归约操作叫做汇总操作:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
@summingInt的工作流程
汇总也指求某一字段的平均数:
double avgCalories =
menu.stream().collect(averagingInt(Dish::getCalories));
求平均值、最大值等所有计算结果:
IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
这个IntSummaryStatistics
类的getter可以获取五个计算结果:
IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}
同样,有Int前缀的特化通常就有Long和Double特化。这里不赘述。
连接字符串
连接所有菜肴名字的joining
操作:
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
如果对对象使用joining
那么就会调用对象的toString
:
String shortMenu = menu.stream().collect(joining());
joining
还可以添加分隔符:
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
广义的归约汇总
上述归约方法基本能满足程序员生理需求,但如果想定义更复杂的归约规则还是要用到reduce
操作:
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
同样reduce
也有方便的重载:
Optional<Dish> mostCalorieDish =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
它相当于:
reduce(0, Function.identity(), (Dish, Dish)-> Dish)
3. 分组
按类型将菜肴分组:
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
下面是输出:
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
MEAT=[pork, beef, chicken]}
@分组流程图
分组不一定非要使用对象的属性,也可以使用如下这种lambda:
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
} ));
多级分组
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;
})
)
);
@多级映射和n维表是等价的
按子组收集数据
其实就是对每个分组进行归约:
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
// output: {MEAT=3, FISH=2, OTHER=4}
groupingBy(f)
其实是groupingBy(f, toList())
的简便写法,所以后续的归约对前文的归约方法都适用。
获取类菜中最高卡路里:
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
maxBy(comparingInt(Dish::getCalories))));
这里返回结果是一个Optional<Dish>
这其实并不太实用,所以Java8还有更简便的写法。
把收集器转换成另一种类型
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
分组还能干很多事
分组求和
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
4. 分区
分区是分组的一个特殊情况,只区分一判断式:
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));
// output: {false=[pork, beef, chicken, prawns, salmon],
// true=[french fries, rice, season fruit, pizza]}
分区的作用
上面的代码乍一看并没有什么鸟用,直接filter
一下也可以,但分区毕竟也是一种分组,如果加入多级分组的分区就看起来有那么一点鸟用了:
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
groupingBy(Dish::getType)));
总之这就相当于SQL语句中的筛选条件,结合各种归约可以进行灵活应用。
Collectors
类的静态工厂方法
工厂方法 | 返回类型 | 用途 |
---|---|---|
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>> |
根据对流中每个项目应用谓词的结果来对项目进行分区 |
2. 收集器接口
收集器接口定义如下:
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
三个泛型定义如下:
- T是流中收集项目的泛型(一般就是
Stream
的尖括号里那个) - A是累加器类型,求和就是
int
,打包成集合就是List<T>
- R是收集操作最终返回的类型
比如toList()
对应的实现类ToListCollector<T>
的声明如下:
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
理解Collector接口声明的方法
要想自定义一个Collector一般就是实现如下四个方法:
- 建立新的结果容器:
supplier
方法 - 将元素归约到第一步的结果容器:
accumulator
方法 - 将归约好的最终结果进行转换:
finisher
方法 - 合并两个结果容器:
combiner
方法
@归约的逻辑步骤
在Collector还有最后一个方法,
characteristics
。它用来定义收集器的行为,关于流是否可以并行归约,以及哪些可以优化的提示。Characteristics
是一个包含三个项目的枚举:
-
UNORDERED
:归约结果不受遍历顺序和累计顺序影响 -
CONCURRENT
:在accumulator
时是否可以多个线程同时调用。如果没有标为UNORDERED
则只会在无序流上使用并行。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。 -
IDENTITY_FINISH
:这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
成型后的ToListCollector
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.indentity();
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT));
}
}
一种不需要实现Collector接口的自定义收集
toList
方法的另一种写法
List<Dish> dishes = menuStream.collect(
ArrayList::new,
List::add,
List::addAll);
这种方式其实很方便,但没那么易读,实现个Collector
接口也容易扩展。
6. 开发你自己的收集器以获得更好的性能
自定义Collector:
package com.suikajy.java8note.ch6_collect;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
public class PrimeCollector implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> {
Map<Boolean, List<Integer>> map = new HashMap<>();
map.put(true, new ArrayList<>());
map.put(false, new ArrayList<>());
return map;
};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (map, i) -> map
.get(isPrime(map.get(true), i))
.add(i);
}
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (map1, map2) -> {
map1.get(true).addAll(map2.get(true));
map2.get(false).addAll(map2.get(false));
return map1;
};
}
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
}
private boolean isPrime(List<Integer> acc, Integer candidate) {
int candidateRoot = (int) Math.sqrt(candidate);
return takeWhile(acc, i -> i < candidateRoot)
.stream()
.noneMatch(i -> candidate % i == 0);
// return acc.stream()
// .filter(i -> i < candidateRoot)
// .noneMatch(i -> candidate % i == 0);
}
// 使用takeWhile来加速filter。因为filter会对流中的每一个数字判断,但其实到了候选数字的根即可停止了。
private static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
int i = 0;
for (A item : list) {
if (!p.test(item)) {
return list.subList(0, i);
}
i++;
}
return list;
}
}
主函数:
public static void main(String[] args) {
//IntStream.rangeClosed(2, 100).filter(i -> {
// int iRoot = (int) Math.sqrt(i);
// return IntStream.rangeClosed(2, iRoot)
// .noneMatch(divider -> i % divider == 0);
//}).forEach(System.out::println);
IntStream.rangeClosed(2, 100).boxed()
.collect(new PrimeCollector())
.get(true)
.forEach(System.out::println);
}
7. 小结
- collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。
- 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
- 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。
- 收集器可以高效地复合起来,进行多级分组、分区和归约。
- 你可以实现Collector接口中定义的方法来开发你自己的收集器。
整理自《Java 8 实战》
网友评论