美文网首页
用流收集数据

用流收集数据

作者: PawsUp | 来源:发表于2019-02-11 12:06 被阅读0次

    CollectionCollectorCollect的区别:

    • CollectionCollection是集合类接口,ListSetMap是它的子接口,这几个接口及他们的实现类在流操作中很常用。
    • Collector:就是收集器,也是一个接口。它的工具类Collectors提供了很多工厂方法(例如groupingBy)创建的收集器.三大主要功能:将流元素归约和汇总为一个值,元素分组,元素分区。
    • collectcollect是一个终端操作(归约操作,就像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.summingLongCollectors.summingDouble方法的作用完全一样,可以用于求和字段为long或double的情况。

      平均数:
      Collectors.averagingIntaveragingLongaveragingDouble可以计算数值的平均数:

      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} 
      

      同样,summarizingLongsummarizingDouble工厂方法有相关的LongSummaryStatisticsDoubleSummaryStatistics类型,适用于收集的属性是原始类型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方法需要三个参数:

    1. 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
    2. 第二个参数就是转换函数,转换为int类型
    3. 数是一个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方法是安全的,只是因为已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如orElseorElseGet来解开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} //结果
    
    1. 把收集器的结果转换为另一种类型,可以使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中的值提取出来

    2. 与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方法在执行规约操作时可以应用哪些优化,比如并行化
            */
      
      1. T是流中要收集的项目的泛型。
      2. A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
      3. R是收集操作得到的对象的类型。
    1. 建立新的结果容器:supplier方法
      supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。

       public Supplier<List<T>> supplier() {
            return () -> new ArrayList<T>();
       } 
       //或
       public Supplier<List<T>> supplier() {
            return ArrayList::new;
       } 
      
    2. 将元素添加到结果容器: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;
       }
      
    3. 对结果容器应用最终转换:finisher方法
      在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数:

       public Function<List<T>, List<T>> finisher() {
           return Function.identity();
       } 
      
    image.png
    1. 合并两个结果容器:combiner方法
      combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并,这样,就可以做到对流进行并行归约了//???

       public BinaryOperator<List<T>> combiner() {
            return (list1, list2) -> {
                  list1.addAll(list2);
                  return list1; }
       } 
      
    2. characteristics方法
      返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。

      Characteristics是一个包含三个项目的枚举:

      • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。

      • CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。

      • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。

    • 自定义收集器:
      //6.6

    相关文章

      网友评论

          本文标题:用流收集数据

          本文链接:https://www.haomeiwen.com/subject/vjzrsqtx.html