分组收集器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()外,其他一些常见的下游收集器如下所示:
- 归约收集器Collectors.reducing()
- 计数Collectors.counting()
- 求和Collectors.summingInt()
- 求平均值Collectors.averagingInt()
- 最大值/最小值Collectors.maxBy()
- 连接字符串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嵌套();
}
}