Java Stream API 数据处理:`Collectors.groupingBy` 与 `Collectors.partitioningBy` 详解
引言
Java 8 引入的 Stream API 极大地简化了集合数据的处理。在处理集合数据时,经常需要根据某些条件将元素进行分类或分组。java.util.stream.Collectors 类提供了两种强大的收集器,用于实现这一目标:groupingBy() 和 partitioningBy()。本文将深入探讨这两个收集器的用法、差异及适用场景。
Collectors.groupingBy():灵活的多维度分组
groupingBy 收集器允许我们根据一个分类函数(Function)对流中的元素进行分组。它将生成一个 Map<K, List<T>>,其中 K 是分类函数的返回值类型(即分组的键),List<T> 是对应键下的所有元素的列表。
示例:根据员工部门进行分组
假设我们有一个员工列表,希望根据他们所在的部门将员工进行分类。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// 假设有一个员工类定义
class Employee {
private String name;
private String department;
private int salary;
public Employee(String name, String department, int salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getDepartment() {
return department;
}
public int getSalary() {
return salary;
}
@Override
public String toString() {
return "Employee{name='" + name + "', department='" + department + "', salary=" + salary + "}";
}
}
public class EmployeeDepartmentGrouping {
public static void main(String[] args) {
List<Employee> allEmployees = new ArrayList<>();
allEmployees.add(new Employee("王华", "研发部", 8000));
allEmployees.add(new Employee("李明", "市场部", 6000));
allEmployees.add(new Employee("张丽", "研发部", 9000));
allEmployees.add(new Employee("赵刚", "人力资源部", 7500));
allEmployees.add(new Employee("钱文", "市场部", 6500));
allEmployees.add(new Employee("孙强", "研发部", 8500));
// 根据部门对员工进行分组
Map<String, List<Employee>> employeesByDept = allEmployees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println("员工按部门分组结果:");
employeesByDept.forEach((deptName, employeeList) -> {
System.out.println("部门: " + deptName);
employeeList.forEach(System.out::println);
});
}
}
/*
预期输出:
员工按部门分组结果:
部门: 市场部
Employee{name='李明', department='市场部', salary=6000}
Employee{name='钱文', department='市场部', salary=6500}
部门: 人力资源部
Employee{name='赵刚', department='人力资源部', salary=7500}
部门: 研发部
Employee{name='王华', department='研发部', salary=8000}
Employee{name='张丽', department='研发部', salary=9000}
Employee{name='孙强', department='研发部', salary=8500}
*/
Collectors.partitioningBy():精准的二元分区
partitioningBy 收集器则用于根据一个谓词(Predicate,即返回 boolean 值的函数)将流中的元素分为严格的两个组:满足条件的(true)和不满足条件的(false)。它总是返回一个 Map<Boolean, List<T>>。
示例:根据薪资是否超过阈值进行分区
从员工列表中找出所有薪资高于某个特定阈值的员工和低于或等于该阈值的员工。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.Arrays; // 引入 Arrays.asList
// 沿用 Employee 类定义
// class Employee { ... }
public class EmployeeSalaryPartition {
public static void main(String[] args) {
List<Employee> staffRecords = Arrays.asList(
new Employee("小李", "销售部", 5500),
new Employee("老王", "技术部", 12000),
new Employee("小张", "销售部", 4800),
new Employee("阿芳", "管理部", 9500),
new Employee("大刘", "技术部", 11000)
);
// 根据薪资是否超过 8000 进行分区
int salaryThreshold = 8000;
Map<Boolean, List<Employee>> salaryGroups = staffRecords.stream()
.collect(Collectors.partitioningBy(emp -> emp.getSalary() > salaryThreshold));
System.out.println("\n员工按薪资分区结果 (高于 " + salaryThreshold + " 元):");
System.out.println("高薪员工 (true): " + salaryGroups.get(true));
System.out.println("普通薪资员工 (false): " + salaryGroups.get(false));
}
}
/*
预期输出:
员工按薪资分区结果 (高于 8000 元):
高薪员工 (true): [Employee{name='老王', department='技术部', salary=12000}, Employee{name='阿芳', department='管理部', salary=9500}, Employee{name='大刘', department='技术部', salary=11000}]
普通薪资员工 (false): [Employee{name='小李', department='销售部', salary=5500}, Employee{name='小张', department='销售部', salary=4800}]
*/
groupingBy 与 partitioningBy 的异同
partitioningBy是groupingBy的一个特例:partitioningBy总是根据一个布尔条件将数据分成true和false两组。groupingBy更通用: 它可以根据任何Function产生多个分组,分组键可以是任意类型。- 语义清晰度: 当只需要将数据一分为二时,
partitioningBy更能清晰地表达这种二分意图,且其Map<Boolean, List<T>>的返回类型在语义上非常明确。 - 内部实现: 尽管可以通过
groupingBy(item -> condition ? "满足" : "不满足")来模拟partitioningBy的效果,但partitioningBy在内部实现上通常会更高效,因为它专门针对布尔分区进行了优化。
Lists.partition() (来自 Guava 库) 的区别
有时开发者可能会混淆 Stream API 中的分区概念与 Guava 库中的 Lists.partition() 方法。
Lists.partition(List<T> list, int size)的作用是将一个大列表按固定大小(size)分割成多个子列表的列表。这是一种"分块"或"批处理"操作,而非基于数据属性的"分组"或"分区"。例如,Lists.partition(Arrays.asList(1,2,3,4,5,6), 2)会得到[[1,2], [3,4], [5,6]]。- 这与 Stream API 的
groupingBy或partitioningBy的目的完全不同。Stream API 的收集器是根据元素的逻辑属性进行分类,而Lists.partition()关注的是列表的物理结构和大小。
封装自定义分区工具方法
为了提高代码复用性,我们可以将 Collectors.partitioningBy 封装成一个独立的静态工具方法:
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.Arrays;
public class CollectionPartitionUtils {
/**
* 根据给定的谓词将列表元素分成两组。
* @param <E> 元素类型。
* @param items 待处理的元素列表。
* @param condition 分区条件(谓词)。
* @return 包含两个列表的Map,键为Boolean(true表示满足条件,false表示不满足)。
*/
public static <E> Map<Boolean, List<E>> divideByCondition(List<E> items, Predicate<E> condition) {
return items.stream()
.collect(Collectors.partitioningBy(condition));
}
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cat", "dog", "elephant", "fox");
// 将单词按长度是否大于4进行分区
Map<Boolean, List<String>> partitionedWords = divideByCondition(
words,
s -> s.length() > 4
);
System.out.println("\n按长度分区 (长度 > 4):");
System.out.println("长单词 (true): " + partitionedWords.get(true));
System.out.println("短单词 (false): " + partitionedWords.get(false));
}
}
/*
预期输出:
按长度分区 (长度 > 4):
长单词 (true): [apple, banana, elephant]
短单词 (false): [cat, dog, fox]
*/