用流收集数据

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容