第五章:用流收集数据
本章内容:
将数据流归约为一个值
汇总:归约的特殊情况
数据分组和分区
开发自己的自定义收集器
5.1 归约与汇总
5.1.1 查找流中的最大值和最小值
如果你想找出菜单中热量最高或最低的菜,你可以使用两个收集器:Collectors.maxBy和Collectors.minBy来计算流中的最大值或最小值
Optional<Dish> maxClories = Dish.menu.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))); if(maxClories.isPresent()){ System.out.println("maxBy : " + maxClories.get().getCalories()); } Optional<Dish> minBy = Dish.menu.stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))); if(minBy.isPresent()) { System.out.println("minBy : " + minBy.get().getCalories()); }
5.1.2 汇总
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可以接受一个把对象映射为求和所需int的函数,并返回一个收集器
Integer sumClories = Dish.menu.stream().collect(summingInt(Dish::getCalories));
summingLong 和 summingDouble 方法的作用和 summingInt 完全一样,可以作用于求和字段为 long 或 double 的情况
但是汇总不仅仅是求和,还有Collectors.averagingInt,连同对应的有 Collectors.averagingLong 和 Collectors.averagingDouble 可以计算数值的平均值
Double avgClories = Dish.menu.stream().collect(averagingInt(Dish::getCalories));
到目前为止,我们可以使用收集器给流中的元素计数,找到他们的最大值和最小值,以及计算出他们的总和及平均值,不过有时候你可能想得到两个或者更多的这样的结果,而且你希望通过一次操作就可以完成。这种情况下您可以使用 Collectors.summarizingInt
IntSummaryStatistics intSummaryStatistics = Dish.menu.stream().collect(summarizingInt(Dish::getCalories)); System.out.println("count : " + intSummaryStatistics.getCount()); System.out.println("sum : " + intSummaryStatistics.getSum()); System.out.println("max : " + intSummaryStatistics.getMax()); System.out.println("min : " + intSummaryStatistics.getMin()); System.out.println("avg : " + intSummaryStatistics.getAverage());
同样 summarizingLong 和 summarizingDouble 适用于收集的属性是原始类型为 long 和 double 的情况
5.1.3 连接字符串
joining 工厂方法返回的收集器会把对流中每一个对象应用 toString 方法得到的所有字符串连接成一个字符串
String names = Dish.menu.stream().map(Dish::getName).collect(joining());
但该字符串的可读性并不好,joining 方法有重载的方法,可以接受元素之间的分界符
names = Dish.menu.stream().map(Dish::getName).collect(joining(", "));
5.2 分组
一个常见的数据库操作是根据一个或者多个属性对集合中的项目进行分组。假如现在需要你把菜单进行分类,可以使用Collectors.groupingBy来实现
Map<Type, List<Dish>> mapByType = Dish.menu.stream().collect(groupingBy(Dish::getType));
groupingBy 方法需要传递一个 Function,这个 Function 我们叫做分类函数,因为它把流中的元素分成不同组。如果DIsh类中没有提供供你使用的分类函数,你也可以将这个逻辑写成Lambda表达式
Map<String, List<Dish>> mapByCal = Dish.menu.stream().collect(groupingBy(d -> { if(d.getCalories() <= 400) { return "低热量"; } else if(d.getCalories() <= 700) { return "中等热量"; } else { return "高热量"; } }));
5.2.1 多级分组
要实现多级分组,可以使用groupingBy的重载方法,除了接受普通的分类函数之外,还可以接受Collector类型的第二个参数。那么想要进行二级分组的话,可以将一个内层groupingBy传递给外层的groupingBy,并定义一个为流中元素分类的二级标准
Map<Type, Map<String, List<Dish>>> mapMap = Dish.menu.stream().collect(groupingBy(Dish::getType, groupingBy(d -> { if(d.getCalories() <= 400) { return "低热量"; } else if(d.getCalories() <= 700) { return "中等热量"; } else { return "高热量"; } })));
5.2.2 按子组收集数据
上面我们传递给第一个groupingBy的第二个收集器还是groupingBy,其实第二个收集器可以使任何收集器类型。例如想统计每类菜种有多少个,可以传递counting收集器作为groupingBy的第二个参数
Map<Type, Long> typeCount = Dish.menu.stream().collect(groupingBy(Dish::getType, counting()));
注意:
其实普通的单参数groupingBy(f)(f为分类函数)实际上是groupingBy(f, toList())的简便写法
再举一个例子:查找出每类菜肴中热量最高的菜肴
分析:
1. 每类菜肴说明需要按菜肴的种类进行分组,使用groupingBy
2. 热量最高的菜肴需要使用maxBy收集器
Map<Type, Optional<Dish>> typeMaxCalMap = Dish.menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
1. 将收集器的结果转换成另一种类型
你可以使用Collectors.collectingAndThen方法返回的收集器
Map<Type, Dish> _typeMaxCalMap = Dish.menu.stream().collect(
groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparing(Dish::getCalories)),
Optional::get)));
这个方法接受两个参数:要转换的收集器和转换函数,并返回另一个收集器
2. 与 groupingBy 联合使用的其他收集器的例子
对每一类菜肴的热量求和
Map<Type, Integer> sumCalMap = Dish.menu.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
常常和 groupingBy 一起使用的另一个收集器是 mapping 方法产生的
Map<Type, Set<String>> mappingMap = Dish.menu.stream().collect(groupingBy(Dish::getType, mapping(d -> { if(d.getCalories() <= 400) { return "低热量"; } else if(d.getCalories() <= 700) { return "中等热量"; } else { return "高热量"; } }, toSet())));
流元素传递给转换函数将Dish映射成了String类型,生成的String类型流传递给一个 toSet 收集器,它和 toList 类似,只是它将流中的元素收集到一个Set集合,而不是List集合,Set集合的元素不重复
通过使用toCollection,你可以有更多的控制
Map<Type, HashSet<String>> toCollectionMap = Dish.menu.stream().collect(groupingBy(Dish::getType, mapping(d -> { if(d.getCalories() <= 400) { return "低热量"; } else if(d.getCalories() <= 700) { return "中等热量"; } else { return "高热量"; } }, toCollection(HashSet::new))));
5.3 分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组的Map的键的类型是Boolean,于是它最多可以分为两组(true和false),例如你现在想把菜肴分为素食和非素食
Map<Boolean, List<Dish>> isVegeMap = Dish.menu.stream().collect(partitioningBy(Dish::isVegetarian));
5.3.1 分区的优势
partitioningBy也有重载的方法,按是否为素食分区,按菜肴类型分组
Map<Boolean, Map<Type, List<Dish>>> isVegeTypeMap = Dish.menu.stream().
collect(
partitioningBy(Dish::isVegetarian,
groupingBy(Dish::getType)));
找出素食与非素食中热量最高的菜肴
Map<Boolean, Dish> vegeMaxCalMap = Dish.menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(
maxBy(
comparingInt(Dish::getCalories)), Optional::get)));
练习:
将数字按质数和非质数分区:现在你要写一个方法,它接受一个参数 int 类型的 n ,并将前 n 个自然数分为质数和非质数
// 判断一个数是否为质数(质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数!) private static boolean isPrime(int n) { return IntStream.range(2, n).noneMatch(i -> n % i == 0); } // 优化 private static boolean isNotPrime(int n) { // 1. 仅测试小于等于测试数字的平方根的因子即可 int sqrt = (int) Math.sqrt(n); // 2. noneMatch将会对所有元素进行匹配,可以使用anyMatch return !IntStream.rangeClosed(2, sqrt).anyMatch(i -> n % i == 0); } // 质数与非质数分区的方法 public static Map<Boolean, List<Integer>> partitionPrimes(int n) { return IntStream.rangeClosed(2, n).boxed().collect(partitioningBy(i -> isNotPrime(i))); }
总结:Collectors类的静态工厂方法
| 工厂方法 | 返回类型 | 用 于 |
| toList | List<T> | 将流中所有项目收集到一个List |
| 使用示例:List<Dish> toList = menuStream.collect(toList()); | ||
| toSet | Set<T> | 将流中所有项目收集到一个Set,删除重复项 |
| 使用示例:Set<Dish> toSet = menuStream.collect(toSet()); | ||
| toCollection | Collection<T> | 把流中所有项目收集到给定的供应源创建的集合 |
| 使用示例:LinkedList<Dish> toCollection = menuStream.collect(toCollection(LinkedList<Dish>::new)); | ||
| counting | Long | 计算流中元素的个数 |
| 使用示例:Long counting = menuStream.collect(counting()); | ||
| summingInt | Integer | 对流中项目的一个整数属性求和 |
| 使用示例:Integer summingInt = menuStream.collect(summingInt(Dish::getCalories)); | ||
| averagingInt | Double | 计算流中项目Integer属性的平均值 |
| 使用示例:Double averagingInt = menuStream.collect(averagingInt(Dish::getCalories)); | ||
| summarizingInt | IntSummaryStatistics | 收集关于流中项目Integer属性的统计值,例如最大值、最小值、总和和平均值 |
| 使用示例:IntSummaryStatistics summarizingInt = menuStream.collect(summarizingInt(Dish::getCalories)); | ||
| joining | String | 连接对流中每个项目电泳toString方法所生成的字符串 |
| 使用示例:String joining = menuStream.map(Dish::getName).collect(joining(", ")); | ||
| maxBy | Optional<T> | 一个按给定比较器选出的最大元素的Optional,如果流为空,则为Optional.empty() |
| 使用示例:Optional<Dish> _maxBy = menuStream.collect(maxBy(comparing(Dish::getCalories))); | ||
| minBy | Optional<T> | 一个按给定比较器选出的最小元素的Optional,如果流为空,则为Optional.empty() |
| 使用示例:Optional<Dish> _minBy = menuStream.collect(minBy(comparing(Dish::getCalories))); | ||
| reducing | 归约操作产生的类型 | 从一个累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,将流归约为单个值 |
| 使用示例:Integer reducing = menuStream.collect(reducing(0, Dish::getCalories, Integer::sum)); | ||
| collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 |
| 使用示例:Integer collectingAndThen = menuStream.collect(collectingAndThen(toList(), List::size)); | ||
| groupingBy | Map<K, List<T>> | 分组 |
| 使用示例:Map<Type, List<Dish>> groupingBy = menuStream.collect(groupingBy(Dish::getType)); | ||
| partitioningBy | Map<Boolean, List<T>> | 分区 |
| 使用示例:Map<Boolean, List<Dish>> partitioningBy = menuStream.collect(partitioningBy(Dish::isVegetarian)); | ||
备注:
摘自文献:《Java8实战》(中文版)《Java8 in Action》(英文版)
代码(GitHub地址): https://github.com/changlezhong/java8InAction
posted on 2018-06-09 22:21 changlezhong 阅读(221) 评论(0) 收藏 举报
浙公网安备 33010602011771号