当前位置: 首页 > news >正文

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;}@Overridepublic 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嵌套();	}
}	
http://www.dtcms.com/a/310748.html

相关文章:

  • 记一次Windwos非常离谱的系统错误,IPF错误,程序构建卡顿,程序启动卡顿。。。
  • 特征工程 --- 特征提取
  • <1> ES内存泄漏问题深度解析:从Scroll查询到Mapped Buffer异常
  • WAIC 2025 聚焦“智能时代”,AI在内容、硬件与交互上的多线突破
  • IFC 转换为 UG 的技术指南及迪威模型网在线转换推荐
  • 签名分发平台怎么看我的订单
  • 从零到一:Linux内核MMU启动与虚拟内存体系建立全流程详解
  • 代码随想录算法训练营三十三天|动态规划part06
  • [Linux入门] Linux 防火墙技术入门:从 iptables 到 nftables
  • 一文了解 `package.json` 和 `package-lock.json`文件
  • Mysql group by
  • 查看主板信息的3种方法
  • 修改DeepSeek翻译得不对的V语言字符串文本排序程序
  • 【ESP32 IDF】LVGL驱动触摸屏
  • AI Agent 视角:可执行程序的二进制格式,是一场「结构化语言」与「智能解析」的双向奔赴
  • 知识图谱的学习
  • 脚本统计MongoDB集合表数据量
  • 思途JSP学习 0801
  • 函数 dirfd 详解
  • 26考研|高等代数:欧几里得空间
  • TwinCAT3示例项目1
  • Redis学习18-分布式锁
  • 深拷贝与浅拷贝的定义
  • 机器学习特征工程----常见的特征构建与转换方法
  • dify 升级1.7.1 插件无法下载依赖
  • 分区管控与高效协同:EtherCAT转EtherCAT网关赋能纺织生产
  • c++-reverse_iterator
  • 什么是 Redis?从基础概念到技术本质的全面解析
  • 微信小程序页面间通信的实现方式
  • 升级的MS1836C HD转CVBS/S-Video转换器