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

发布于:2025-08-02 ⋅ 阅读:(12) ⋅ 点赞:(0)

分组收集器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嵌套();	
	}
}