深入解析:Java函数式编程之【Stream终止操作】【下】【三】【收集操作collect()与分组分区和下游收集器】

分组收集器groupingBy():groupingBy()收集器用于按条件对元素象进行分组,并将结果存储在Map实例中。其作用与数据库的SQL语句的group by的用法有异曲同工之妙。
分区收集器partitioningBy():partitioningBy()可以看作是分组groupingBy()的特殊情形,实质是在做二分组。它根据断言(Predicate,谓词)将Stream中的元素收集到一个Map实例中;该Map将断言测试结果作为key(键),其key(键)是true/false,可以将流中的元素分为true和false两部分,而value(值)是由满足/不满足断言的元素构成的列表。
Java 核心库中Collectors类中给出的分组和分区操作的样例代码:

/**
* // Group employees by department
* Map<Department, List<Employee>> byDept
  * = employees.stream()
  * .collect(Collectors.groupingBy(Employee::getDepartment));
  *
  * // Compute sum of salaries by department
  * Map<Department, Integer> totalByDept
    * = employees.stream()
    * .collect(Collectors.groupingBy(Employee::getDepartment,
    * Collectors.summingInt(Employee::getSalary)));
    *
    * // Partition students into passing and failing
    * Map<Boolean, List<Student>> passingFailing =
      * students.stream()
      * .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
      *
    * }</pre>
    *
    * @since 1.8
    */

首先,我们定义一个后文要用到的Employee类:

package test;
import java.util.function.Predicate;
public class Employee
{
private String name;
private String gender;
//性别
private int age;
private int salary;
//月工资
private String department;
private String subCompany;
public Employee(String name,String gender,int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public Employee(String name,String gender, int age, int salary,String subComp,String dept) {
this(name, gender, age);
this.salary = salary;
subCompany = subComp;
department = dept;
}
public Integer getAge() {
return age;
}
public String getGender() {
return (gender.equalsIgnoreCase("M")) ? "男":"女";
}
public String getName() {
return name;
}
public int getSalary() {
return salary;
}
public String getDepartment() {
return department;
}
public String getSubCompany() {
return subCompany;
}
@Override
public String toString() {
String sex = getGender();
return "{姓名:"+name+" "+sex+" "+age+" }";
}
//对于常用的谓词逻辑(断言),可在主语实体中定义。如本例Employee中定义以下断言:
public static Predicate<Employee> 老年人 = x -> x.getAge() >
  60;
  public static Predicate<Employee> 男性 = p ->
    "男".equals(p.getGender());
    public static Predicate<Employee> 成年人 = e -> e.getAge() >
      18;
      } //Employee定义结束。

收集操作collect中分组收集器(groupingBy)的用法
一、分组收集器groupingBy()
用于按条件对元素对象进行分组,并将结果存储在Map实例中。其作用与数据库的SQL语句的group by的用法有异曲同工之妙。
分组收集器groupingBy()会返回一个Map,它有两个关键要素,即分组器函数和值收集器:

  • 分组器函数:classifier函数,对流中的元素进行处理,返回一个用于分组键值key,根据key将元素分配到组里。
  • 值收集器:是对于分组后的数据元素的进一步处理转换逻辑容器,此容器是一种Collector收集器,和collect()方法中传入的收集器参数完全等同,实际上就是一个下游收集器,像俄罗斯套娃一样可循环嵌套。

对于分组收集器groupingBy而言,分组器函数与值收集器二者缺一不可。
分组收集器groupingBy共有三种重载形式:

  • public static <T, K> Collector<T, ?, Map<K, List>> groupingBy(Function<? super T, ? extends K> classifier)
    这是单参数的重载形式,有一个参数classifier 是分类器函数。将分类器函数应用于Stream中的数据元素产生键key,根据键key把元素或映射后作为值value放入对应的值收集器。此重载相当于groupingBy(classifier,toList())。请看示例,把公司雇员按部门进行分组,其中Employee::getDepartment是分类器函数:
Map<Department, List<Employee>> byDept = employees.stream()
  .collect(Collectors.groupingBy(Employee::getDepartment));
  • public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
    这是两个参数的重载形式,除了参数classifier分类器函数,还有一个下游收集器downstream参数。此分组收集器会返回一个映射表map,将分类器函数应用于Stream中的数据元素产生键key,而值value是由下游收集器收集。请看示例,按部门分组计算工资汇总:
Map<Department,Integer> totalByDept = employees.stream()
  .collect(Collectors.groupingBy(Employee::getDepartment,Collectors.summingInt(Employee::getSalary)));

下面这个示例:按城市个子最高的人:

Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
  Map<City, Person> tallestByCity = people.stream()
    .collect(groupingBy(Person::getCity, reducing(BinaryOperator.maxBy(byHeight))));
  • public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,Supplier mapFactory,Collector<? super T, A, D> downstream)
    这是叁个参数的重载形式,除了classifier分类器和downstream下游收集器参数外,又新增了一个map供给器参数mapFactory。此分组收集器会返回一个映射表map,将分类器函数应用于Stream中的数据元素产生键key,而与key对应的值value则由下游收集器收集。请看示例,按部门把公司员工分组,收集器定制使用LinkedHashMap,下游收集器使用ArrayList。
Map<Department, List<Employee>> byDept = employees.stream()
  .collect(groupingBy(Employee::getDepartment),LinkedHashMap::new,Arraylist::new);

参数说明:
参数classifier,是Function接口的实例,是分类器。
参数mapFactory,是Supplier接口的实例,是供给器(生产map容器)
参数downstream,是Collector接口的实例,是下游收集器

收集器groupingBy()都是在数据收集前分组,然后再将分好组的数据传递给下游的收集器。两个参数和叁个参数的版本都可适用于Collector嵌套。

收集器groupingBy()有个兄弟groupingByConcurrent(),用法相似。两者区别也仅仅是单线程和多线程的使用场景,groupingByConcurrent()是并发的分组收集器,是线程安全的,可用于多线程场景。

【例程10-31】相同字母异序词测试程序AnagramTest的collect分组收集版本
下面我们先来研究一个相同字母异序词测试程序。
程序说明:程序中用到下面的方法alphabetize()

private static String alphabetize( String str ) {
//按字母顺序重组字符串
char[] array = str.toCharArray();
//把输入字符串分解为字母数组
Arrays.sort(array);
//按字母顺序排序
return new String(array);
//返回按字母顺序重组的字符串
}

这个方法先把输入的字符串分解为字母数组,然后再返回按字母顺序重组的字符串。它的作用相当于给“相同字母异序词”生成一个key。例如,异序词"ear"、“are”、“era"经此方法处理后生成的key为"aer”。
例程中定义了一个映射:

Map<String, TreeSet<String>> wsMap = new HashMap<
  >
  ();

映射wsMap的键key类型是String,值value是树集(TreeSet)。
例程使用了Map方法computeIfAbsent(K key, Function remappingFunction),该方法有两个参数:第一个参数是HashMap的key;第二个参数又称之为重映射函数,用于重新计算value值,本例中这个value是一个TreeSet。
方法computeIfAbsent的作用是:如果HashMap中不存在指定的key键,由重新映射函数计算一个新的value值(创建一个新的TreeSet),然后插入键值对(key,value)至HashMap中,同时返回value值。如果HashMap中已存在key值,该方法只需返回查询到的value值。当HashMap中已存在key值时,该方法的作用相当于get(Object key),实际上是返回一个TreeSet的句柄。
【例程】AnagramTest.java开始:

import java.util.*;
import static java.util.stream.Collectors.*;
/**增加程序可读性**/
/***
* @author QiuGen
* @description 异序词例程AnagramTest的collect分组收集版本
* @date 2024/8/26
* ***/
public class AnagramTest
{
/**相同字母异序词测试**/
static final List<String> WdList = Arrays.asList("ear",
  "are","triangle","integral","three","htree",
  "staple","petals","there","era");
  private static String alphabetize( String str ) {
  //按字母顺序重组字符串
  char[] array = str.toCharArray();
  //把输入字符串分解为字母数组
  Arrays.sort(array);
  //按字母顺序排序
  return new String(array);
  //返回按字母顺序重组的字符串
  }
  public static void TestByMap() {
  /**面向对象编程,使用Map集合的外部迭代**/
  Map<String, TreeSet<String>> wsMap = new HashMap<
    >
    ();
    /**使用集合forEach的写法* k->new TreeSet<>()*不能用*TreeSet::new*奇怪*/
    WdList .forEach(w->
    {
    wsMap.computeIfAbsent(alphabetize(w), k->
    new TreeSet<
    >
    ()).add(w);
    });
    wsMap.forEach( (k,set)-> System.out.println(set) );
    //Map的forEach有二个参数
    }
    public static void TestByStream() {
    /**函数式编程,演示三种集合收集器**/
    WdList .stream().collect( groupingBy(word->
    alphabetize(word)) ) //默认使用List
    .values().stream().forEach(System.out::println);
    System.out.println("**************");
    //使用默认的Set下游收集器
    WdList.stream().collect( groupingBy(word->
    alphabetize(word), toSet()) )
    .values().stream().forEach(System.out::println);
    System.out.println("**************");
    //使用定制的TreeSet收集器
    WdList.stream().collect( groupingBy(word->
    alphabetize(word),
    toCollection(TreeSet::new)) ).values().stream().forEach(System.out::println);
    }
    public static void main(String[] args) {
    //用二种写法的比较测试
    TestByStream();
    /**函数式编程,演示三种集合收集器**/
    System.out.println("---------------");
    TestByMap();
    }
    }

二、分区收集器partitioningBy()

分区收集器partitioningBy()可以看作是分组groupingBy()的特殊情形,其实质根据断言(Predicate)结果分成二组。当分组收集器groupingBy()的分类器classifier返回值为布尔值时,则效果等同于一个分区收集器。

在这里插入图片描述

收集器partitioningBy()根据断言将Stream中的元素收集到一个Map中;该Map把断言测试结果作为key(键),根据key是true/false,可以将流中的元素分为true和false两部分,而value(值)通常是由满足/不满足断言的元素构成的列表。partitioningBy()收集器有两种重载形式:

  • public static Collector<T,?,Map<Boolean,List<>>> partitioningBy(Predicate<? super T>predicate)
    单个参数重载形式,其参数是一个断言predicate。实质上它是双参数重载形式的特殊形式,相当于双参数的:
partitioningBy(predicate, toMap())

下面的示例:按考试成绩是否及格分区(类),收集到列表中

Map<Boolean, List<Student>> passingFailing = students.stream()
  .collect(Collectors.partitioningBy (s -> s.getGrade() >= PASSED));
  • public static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
    Collector<? super T, A, D> downstream)
    双参数的重载形式,新增的第二个参数downstream是下游收集器。下面的示例:按考试成绩是否及格分区,收集到集Set里,指定的收集器是Set。
Map<Boolean, Set<Student>> map = students.stream()
  .collect(partitioningBy(s -> s.getGrade() >= PASSED, toSet()));

三、映射属性收集器mapping()的用法

Collectors.mapping()也是与下游收集器相关的映射收集器,它允许你在收集过程中对流中的每个元素应用一个函数,并将其结果收集起来。使用mapping()可自定义要收集的元素类型,这是一个很有用的功能。

  • public static <T, U, A, R>
    Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
    Collector<? super U, A, R> downstream)

下面的示例:把Person映射转换为字符串类型name后再收集到列表中:

List<String> nameList = personList.stream().collect(mapping(Person::getName, toList()));

亦可先map()映射再收集,实现相同的功能:

List<String> nameList = personList.stream().map(Person::getName).collect(toList());

四、下游收集器(Collector)
下游收集器(Downstream Collector)是Java Stream API中的一个重要概念,它允许在分组(grouping)或分区(partitioning)操作后进行更复杂的收集操作。

下游收集器通常出现在以下收集器方法中:

  • Collectors.groupingBy() 的第二个参数
  • Collectors.partitioningBy() 的第二个参数
  • Collectors.mapping() 的第二个参数

下游收集器用于在主要收集操作完成后,对每个组或分区中的元素执行的进一步收集操作。
下游收集器除了上文介绍的分组收集器groupingBy()、分区收集器partitioningBy()和映射属性收集器Collectors.mapping()外,其他一些常见的下游收集器如下所示:

  1. 归约收集器Collectors.reducing()
  2. 计数Collectors.counting()
  3. 求和Collectors.summingInt()
  4. 求平均值Collectors.averagingInt()
  5. 最大值/最小值Collectors.maxBy()
  6. 连接字符串Collectors.joining()

请看示例:

//reducing() 特别适合作为分组后的下游收集器
// 按城市分组,计算每个城市的最高工资
Map<String, Integer> maxSalaryByCity = employees.stream()
  .collect(Collectors.groupingBy(
  Employee::getCity,
  Collectors.reducing(
  0,
  Employee::getSalary,
  Integer::max
  )
  ));
  Map<String, Long> countByGroup = list.stream()
    .collect(Collectors.groupingBy(Item::getCategory, Collectors.counting()));
    Map<String, Integer> sumByGroup = list.stream()
      .collect(Collectors.groupingBy(Item::getCategory,
      Collectors.summingInt(Item::getPrice)));
      Map<String, Double> avgByGroup = list.stream()
        .collect(Collectors.groupingBy(Item::getCategory,
        Collectors.averagingInt(Item::getPrice)));
        Map<String, Optional<Item>> maxByGroup = list.stream()
          .collect(Collectors.groupingBy(Item::getCategory,
          Collectors.maxBy(Comparator.comparing(Item::getPrice))));
          Map<String, Set<String>> namesByGroup = list.stream()
            .collect(Collectors.groupingBy(Item::getCategory,
            Collectors.mapping(Item::getName, Collectors.toSet())));
            Map<String, String> joinedNames = list.stream()
              .collect(Collectors.groupingBy(Item::getCategory,
              Collectors.mapping(Item::getName, Collectors.joining(", "))));

下游收集器大大增强了Java Stream API的数据处理能力,使得复杂的数据聚合操作变得简洁而高效。

五、收集器(Collector)的嵌套

有的时候,我们需要先根据某个维度进行分组,然后再根据第二维度进一步的分组,然后再对分组后的结果进一步的进行处理。这种应用场景,我们可以通过分组分区和映射收集器,再组合其他下游收集器(Collector)的叠加嵌套来实现。这种嵌套像可以俄罗斯套娃一样层层嵌套。Employee类定义请参见本节开头。
【例程10-32】Collector使用综合例程“Collect分组分区Demo”

package test;
import static java.util.stream.Collectors.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;
public class Collect分组分区Demo {
public static List<Employee>
  empList() {
  List<Employee> list = new ArrayList<
    >
    ();
    list.add(new Employee("刘敏","f", 28, 2000,"宁波分公司","销售部"));
    list.add(new Employee("李伟","M", 44, 4060,"上海分公司","营销部"));
    list.add(new Employee("丁丽","F", 55, 5050,"上海分公司","工程部"));
    list.add(new Employee("赵云","m" , 66, 6080,"宁波分公司","营销部"));
    list.add(new Employee("张三","M", 33, 3300,"上海分公司","工程部"));
    list.add(new Employee("钱玄同","m", 23, 2080,"上海分公司","销售部"));
    return list;
    }
    public static void collectGroupingBy() {
    // 对“上海分公司”的员工,按部门分组
    Map<String, List<Employee>> resultMap = empList().stream()
      .filter(e ->
      "上海分公司".equals(e.getSubCompany()))
      .collect(groupingBy(Employee::getDepartment));
      }
      public static void collectPartitioningBy() {
      // 分区示例
      Map<Boolean,List<Employee>> map = empList().stream()
        .filter(e ->
        "宁波分公司".equals(e.getSubCompany()))
        .collect(partitioningBy(e->e.getAge() >
        40));
        System.out.println("----按工资>=4000 分区:打印分区结果----");
        Map<Boolean, Set<Employee>> setMap = empList().stream()
          .collect(partitioningBy(e -> e.getSalary() >= 4000, toSet()));
          setMap.forEach((k,v) -> System.out.println("键:" + k + ", 值:" + v));
          }
          public static void collectReducing() {
          /***reducing示例***/
          /***统计每个分公司年龄最大的员工***/
          Comparator<Employee> cAge = Comparator.comparing(Employee::getAge);
            Optional<Employee> employeeOpt = empList().stream()
              .filter(e ->
              "上海分公司".equals(e.getSubCompany()))
              .collect(reducing(BinaryOperator.maxBy(cAge)));
              //寻找上海分公司中年龄最大的员工:对收集器结果进行转换整理
              Employee employee = empList().stream()
              .filter(e ->
              "上海分公司".equals(e.getSubCompany()))
              .collect(collectingAndThen(reducing(BinaryOperator.maxBy(cAge)),Optional::get));
              System.out.println(employee);
              }
              public static void collectMaping() {
              /***mapping示例***/
              //例如,获取Employee姓名列表:
              System.out.println("---获取Employee姓名列表:---");
              List<String> namelist = empList().stream()
                .collect(mapping(Employee::getName, toList()));
                namelist.forEach(System.out::println);
                System.out.println("--------------------");
                /***下面使用map()映射后再collect(),实现相同功能,更简明。***/
                namelist = empList().stream()
                .map(Employee::getName).collect(toList());
                namelist.forEach(System.out::println);
                }
                public static void collectingAndThenTest() {
                /***collectingAndThen示例***/
                // 先按工资再按年龄升序排序
                List<String> nameList = empList().stream()
                  .sorted(Comparator.comparing(Employee::getSalary).thenComparing(Employee::getAge))
                  .map(Employee::getName).collect(Collectors.toList());
                  Employee employee = empList().stream()
                  .filter(emp ->
                  "上海分公司".equals(emp.getSubCompany()))
                  .collect(collectingAndThen(
                  maxBy(Comparator.comparing(Employee::getSalary)), Optional::get));
                  // 将员工先按分公司分组,再对分公司员工排序后放入列表
                  Map<String, List<Employee>> map = empList().stream()
                    .collect(groupingBy(Employee::getSubCompany, collectingAndThen(
                    toCollection(() ->
                    new TreeSet<
                    >
                    (Comparator.comparing(Employee::getSalary))),
                    ArrayList::new)));
                    System.out.println("打印显示map的内容");
                    for (String dept : map.keySet()) {
                    map.get(dept).stream().forEach(e->System.out.println(e.getName()));
                    }
                    // 将员工先排序,再按分公司分组,效率低
                    Map<String, List<Employee>> map3 = empList().stream()
                      .sorted(Comparator.comparing(Employee::getSalary))
                      .collect(groupingBy(Employee::getSubCompany));
                      System.out.println("打印显示map3的内容");
                      for (String dept : map3.keySet()) {
                      map3.get(dept).stream().forEach(e->System.out.println(e.getName()));
                      }
                      }
                      public static void collect嵌套() {
                      /***将员工先按分公司分组,再把Employee列表映射为姓名列表。***/
                      Map<String, List<String>> mapN = empList().stream()
                        .collect(groupingBy(Employee::getSubCompany,
                        mapping(Employee::getName,toList())));
                        /***将员工先按分公司分组,再按部门分组。***/
                        Map<String, Map<String, List<Employee>>
                          > map = empList().stream()
                          .collect(groupingBy(Employee::getSubCompany,
                          groupingBy(Employee::getDepartment)));
                          /***按分公司汇总工资***/
                          Map<String, Integer> sumSalary = empList().stream()
                            .collect(groupingBy(Employee::getSubCompany,
                            summingInt(Employee::getSalary)));
                            /***按照分公司+部门两个维度,统计各个部门人数。叁层嵌套***/
                            Map<String, Map<String, Long>> rtnMap = empList().stream()
                              .collect(groupingBy(Employee::getSubCompany,
                              groupingBy(Employee::getDepartment,counting())));
                              System.out.println(rtnMap);
                              /***将员工先按分公司分组,再按性别分组。***/
                              Map<String, Map<Boolean, List<Employee>>
                                > map2 = empList().stream()
                                .collect(groupingBy(Employee::getSubCompany, partitioningBy(Employee.男性)));
                                /***将员工按性别分组统计***/
                                Map<Boolean, Long> rstMap = empList().stream()
                                  .collect(partitioningBy(Employee.男性, counting()));
                                  rstMap.forEach((k,v) -> System.out.println("键:" + k + ", 值:" + v));
                                  /***将员工先按分公司分组,再求各分公司工资最高的员工***/
                                  Map<String, Employee> map3 = empList().stream()
                                    .collect(groupingBy(Employee::getSubCompany, collectingAndThen(
                                    maxBy(Comparator.comparing(Employee::getSalary)), Optional::get)));
                                    /***统计每个分公司年龄最大的员工***/
                                    Comparator<Employee> cAge = Comparator.comparing(Employee::getAge);
                                      Map<String, Optional<Employee>> oldestSubCompany = empList().stream()
                                        .collect(groupingBy(Employee::getSubCompany, reducing(BinaryOperator.maxBy(cAge))));
                                        /***先按分公司分组,再寻找年龄最大的员工***/
                                        Map<String, Employee > mapOldest = empList().stream()
                                          .collect(groupingBy(Employee::getSubCompany,
                                          collectingAndThen(reducing(BinaryOperator.maxBy(cAge)),Optional::get)));
                                          }
                                          public static void main(String[] args) {
                                          collectingAndThenTest();
                                          //collectPartitioningBy();
                                          collect嵌套();
                                          }
                                          }
posted @ 2025-08-03 18:50  wzzkaifa  阅读(30)  评论(0)    收藏  举报