• Java8中聚合操作collect、reduce方法详解


    Stream的基本概念

    Stream和集合的区别:

    Stream不会自己存储元素。元素储存在底层集合或者根据需要产生。
    Stream操作符不会改变源对象。相反,它会返回一个持有结果的新的Stream。
    3.Stream操作可能是延迟执行的,这意味着它们会等到需要结果的时候才执行。
    Stream操作的基本过程,可以归结为3个部分:

    创建一个Stream。
    在一个或者多个操作中,将指定的Stream转换为另一个Stream的中间操作。
    通过终止(terminal)方法来产生一个结果。该操作会强制它之前的延时操作立即执行,这之后该Stream就不能再被使用了。
       中间操作都是filter()、distinct()、sorted()、map()、flatMap()等,其一般是对数据集的整理(过滤、排序、匹配、抽取等)。

        终止方法往往是完成对数据集中数据的处理,如forEach(),还有allMatch()、anyMatch()、findAny()、 findFirst(),数值计算类的方法有sum、max、min、average等等。终止方法也可以是对集合的处理,如reduce()、 collect()等等。reduce()方法的处理方式一般是每次都产生新的数据集,而collect()方法是在原数据集的基础上进行更新,过程中不产生新的数据集。

    Listnums = Arrays.asList(1, 3, null, 8, 7, 8, 13, 10);
    nums.stream().filter(num -> num != null).distinct().forEach(System.out::println);
      上面代码实现为过滤null值并去重,遍历结果,实现简洁明了。使用传统方法就相对繁琐的多。另外其中 forEach即为终止操作方法,如果无该方法上面代码就没有任何操作。filter、map、forEach、findAny等方法的使用都比较简单,这里省略。

    下面介绍强大的聚合操作,其主要分为两种:

    可变聚合:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
    其他聚合:除去可变聚合,剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;
    聚合操作reduce

        Stream.reduce,返回单个的结果值,并且reduce操作每处理一个元素总是创建一个新值。常用的方法有average, sum, min, max, count,使用reduce方法都可实现。这里主要介绍reduce方法:

    T reduce(T identity, BinaryOperatoraccumulator)
       identity:它允许用户提供一个循环计算的初始值。accumulator:计算的累加器,其方法签名为apply(T t,U u),在该reduce方法中第一个参数t为上次函数计算的返回值,第二个参数u为Stream中的元素,这个函数把这两个值计算apply,得到的和会被赋值给下次执行这个方法的第一个参数。有点绕看代码:

    int value = Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum + item);
    Assert.assertSame(value, 110);
    /* 或者使用方法引用 */
    value = Stream.of(1, 2, 3, 4).reduce(100, Integer::sum);
      这个例子中100即为计算初始值,每次相加计算值都会传递到下一次计算的第一个参数。

    reduce还有其它两个重载方法:

    Optionalreduce(BinaryOperatoraccumulator):与上面定义基本一样,无计算初始值,所以他返回的是一个Optional。
    U reduce(U identity, BiFunction<u,? u="" super=""> accumulator, BinaryOperator combiner):与前面两个参数的reduce方法几乎一致,你只要注意到BinaryOperator其实实现了BiFunction和BinaryOperator两个接口。
    收集结果collect

      当你处理完流时,通常只是想查看一下结果,而不是将他们聚合为一个值。先看collect的基础方法,它接受三个参数:

    R collect(Suppliersupplier, BiConsumer<r,? t="" super="">accumulator, BiConsumer<r,r>combiner)
    supplier:一个能创造目标类型实例的方法。accumulator:一个将当元素添加到目标中的方法。combiner:一个将中间状态的多个结果整合到一起的方法(并发的时候会用到)。接着看代码:

    Streamstream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);
    Listresult = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));
    /* 或者使用方法引用 */
    result = stream.collect(ArrayList::new, List::add, List::addAll);
    这个例子即为过滤大于2的元素,将剩余结果收集到一个新的list中。

    第一个方法生成一个新的ArrayList;
    第二个方法中第一个参数是前面生成的ArrayList对象,第二个参数是stream中包含的元素,方法体就是把stream中的元素加入ArrayList对象中。第二个方法被反复调用直到原stream的元素被消费完毕;
    第三个方法也是接受两个参数,这两个都是ArrayList类型的,方法体就是把第二个ArrayList全部加入到第一个中;
    代码有点繁琐,或者使用collect的另一个重载方法:

    <r,a>R collect(Collector collector)
    注意到Collector其实是上面supplier、accumulator、combiner的聚合体。那么上面代码就变成:

    Listlist = Stream.of(1, 2, 3, 4).filter(p -> p > 2).collect(Collectors.toList());
    将结果收集到map中

    先定义如下Person对象

    class Person{
        public String name;
        public int age;

        Person(String name, int age){
          this.name = name;
          this.age = age;
        }

        @Override
        public String toString(){
          return String.format("Person{name='%s', age=%d}", name, age);
        }
      }
    假设你有一个Stream对象,希望将其中元素收集到一个map中,这样就可以根据他的名称来查找对应年龄,例如:

    Map<string, integer="">result = people.collect(HashMap::new,(map,p)->map.put(p.name,p.age),Map::putAll);
    /*使用Collectors.toMap形式*/
    Map<string, integer="">result = people.collect(Collectors.toMap(p -> p.name, p -> p.age, (exsit, newv) -> newv));
    其中Collectors.toMap方法的第三个参数为键值重复处理策略,如果不传入第三个参数,当有相同的键时,会抛出一个IlleageStateException。

    或者你想将Person分解为Map存储:

    List<Map<string, object="">> personToMap = people.collect(ArrayList::new, (list, p) -> {
       Map<string, object="">map = new HashMap<>();
       map.put("name", p.name);
       map.put("age", p.age);
       list.add(map);
    }, List::addAll);
    分组和分片

    对具有相同特性的值进行分组是一个很常见的任务,Collectors提供了一个groupingBy方法,方法签名为:

    <t,k,a,d>Collector<T,?,Map<k,d>> groupingBy(Function classifier, Collector downstream)
    classifier:一个获取Stream元素中主键方法。downstream:一个操作对应分组后的结果的方法。

    假如要根据年龄来分组:

    Map<Integer, List> peropleByAge = people.filter(p -> p.age > 12).collect(Collectors.groupingBy(p -> p.age, Collectors.toList()));
    假如我想要根据年龄分组,年龄对应的键值List存储的为Person的姓名,怎么做呢:

    Map<Integer, List> peropleByAge = people.collect(Collectors.groupingBy(p -> p.age, Collectors.mapping((Person p) -> p.name, Collectors.toList())));
    mapping即为对各组进行投影操作,和Stream的map方法基本一致。

    假如要根据姓名分组,获取每个姓名下人的年龄总和(好像需求有些坑爹):

    Map<string, integer="">sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name, Collectors.reducing(0, (Person p) -> p.age, Integer::sum)));
    /* 或者使用summingInt方法 */
    sumAgeByName = people.collect(Collectors.groupingBy(p -> p.name, Collectors.summingInt((Person p) -> p.age)));
    可以看到Java8的分组功能相当强大,当然你还可以完成更复杂的功能。另外Collectors中还存在一个类似groupingBy的方法:partitioningBy,它们的区别是partitioningBy为键值为Boolean类型的groupingBy,这种情况下它比groupingBy更有效率。

    join和统计功能

    话说Java8中新增了一个StringJoiner,Collectors的join功能和它基本一样。用于将流中字符串拼接并收集起来,使用很简单:

    String names = people.map(p->p.name).collect(Collectors.joining(","))
    Collectors分别提供了求平均值averaging、总数couting、最小值minBy、最大值maxBy、求和suming等操作。但是假如你希望将流中结果聚合为一个总和、平均值、最大值、最小值,那么Collectors.summarizing(Int/Long/Double)就是为你准备的,它可以一次行获取前面的所有结果,其返回值为(Int/Long/Double)SummaryStatistics。

    DoubleSummaryStatistics dss = people.collect(Collectors.summarizingDouble((Person p)->p.age));
    double average=dss.getAverage();
    double max=dss.getMax();
    double min=dss.getMin();
    double sum=dss.getSum();
    double count=dss.getCount();

  • 相关阅读:
    【算法学习笔记】27.动态规划 解题报告 SJTU OJ 1254 传手绢
    【算法学习笔记】26.扫描维护法 解题报告 SJTU OJ 1133 数星星
    【算法学习笔记】25.贪心法 均分纸牌问题的分析
    【算法学习笔记】24.记忆化搜索 解题报告 SJTU OJ 1002 二哥种花生
    【算法学习笔记】23.动态规划 解题报告 SJTU OJ 1280 整装待发
    【算法学习笔记】22.算法设计初步 二分查找 上下界判断
    【算法学习笔记】21.算法设计初步 求第k个数 划分法 快排法
    【算法学习笔记】20.算法设计初步 归并排序 求逆序数
    【算法学习笔记】19.算法设计初步 最大子列和问题的几种方法
    【算法学习笔记】18.暴力求解法06 隐式图搜索2 八数码问题 未启发
  • 原文地址:https://www.cnblogs.com/hujihon/p/6612851.html
Copyright © 2020-2023  润新知