用流收集数据

CollectionCollectorCollect的区别:

  • CollectionCollection是集合类接口,ListSetMap是它的子接口,这几个接口及他们的实现类在流操作中很常用。
  • Collector:就是收集器,也是一个接口。它的工具类Collectors提供了很多工厂方法(例如groupingBy)创建的收集器.三大主要功能:将流元素归约和汇总为一个值,元素分组,元素分区。
  • collectcollect是一个终端操作(归约操作,就像reduce一样可以接受各种作法作为参数,将流中的元素累积成一个汇总结果),它接受一个收集器作为参数。
  • 查找流中的最大值与最小值

可以使用一个收集器,Collector.maxBy来计算流中的最大值,这个收集器接收一个Comparator参数来比较流中的元素。Optional是考虑到返回值为空时情况。

    Comparator dishCaloriesComparator =
    Comparator.comparingInt(Dish::getCalories); 

    Optional mostCalorieDish =
                 menu.stream()
                     .collect(maxBy(dishCaloriesComparator)); 
  //或者
  Optional 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,也就是一个BiFunction,这就意味着他需要的函数必须接收两个参数,然后返回一个相同类型的值,将两个项目累积成一个同类型的值。

同样也有单参数reduceing工厂方法创建的容器收集器看作是三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点;因此返回一个Optional对象。
例如:

  Optional 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,以便在空流的情况下安全地执行归约操作。然后只需用Optional对象中的get方法来提取里面的值就行了。在这种情况下使用get方法是安全的,只是因为已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如orElseorElseGet来解开Optional中包含的值更为安全。更简单的方法就是映射到一个IntStream,然后调用sum方法。即:

  int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum(); 

可以看出来,其实收集器在某种程度上比Stream接口上直接提供的方法用起来更加复杂,但是使用收集器的好处在于能更容易重用和自定义。

  • 分组
    分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。

    多级分组:

    Map>> 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 typesCount = menu.stream().collect(
                     groupingBy(Dish::getType, counting())); 

  {MEAT=3, FISH=2, OTHER=4} //结果
  1. 把收集器的结果转换为另一种类型,可以使Collectors.collectingAndThen工厂方法返回的收集器

    例如:

     Map mostCaloricByType =
          menu.stream()
              .collect(groupingBy(Dish::getType,//分类函数
                      collectingAndThen(
                          maxBy(comparingInt(Dish::getCalories)),//包装后的收集器 Optional
                      Optional::get))); //转换函数
     //结果
     {FISH=salmon, OTHER=pizza, MEAT=pork}  
    

    这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。collect操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来

  2. 与groupingBy联合使用的其他收集器---mapping

这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。

  Map> 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> partitionedMenu =
                 menu.stream().collect(partitioningBy(Dish::isVegetarian)); 
    //通过分组也能够实现:
    Map> 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 把流中所有项目收集到一个 List
toSet Set 把流中所有项目收集到一个 Set,删除重复项
toCollection Collection 把流中所有项目收集到给定的供应源创建的集合
counting Long 计算流中元素的个数
summingInt Integer 对流中项目的一个整数属性求和
averagingInt Double 计算流中项目 Integer 属性的平均值
summarizingInt IntSummaryStatistics 收集关于流中项目Integer 属性的统计值,例如最大、最小、总和与平均值
joining String 连接对流中每个项目调用 toString 方法所生成的字符串
maxBy Optional 一个包裹了流中按照给定比较器选出的最大元素的 Optional,或如果流为空则为 Optional.empty()
minBy Optional 一个包裹了流中按照给定比较器选出的最小元素的 Optional,或如果流为空则为 Optional.empty()
reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素逐个结合,从而将流归约为单个值
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果应用转换函数
groupingBy Map> 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果 Map 的键
partitioningBy Map> 根据对流中每个项目应用谓词的结果来对项目进行分区

  1. 建立新的结果容器:supplier方法
    supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。

     public Supplier> supplier() {
          return () -> new ArrayList();
     } 
     //或
     public Supplier> supplier() {
          return ArrayList::new;
     } 
    
  2. 将元素添加到结果容器:accumulator方法
    accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。

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

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

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

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

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

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

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

  • 自定义收集器:
    //6.6

你可能感兴趣的:(用流收集数据)