第五章:用流收集数据

本章内容:

  将数据流归约为一个值

  汇总:归约的特殊情况

  数据分组和分区

  开发自己的自定义收集器

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));

  summingLongsummingDouble 方法的作用和 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)    收藏  举报

导航