1. 概述
本教程将探讨如何使用Streams API对存储在Map中的List元素进行排序。过程中,我们会将其与传统的List#sort(Comparator)方法进行对比,分析哪种方式更高效。
2. 问题场景
先明确问题再动手解决。
假设有一个Employee类:
public class Employee {
private String name;
private int salary;
private String department;
private String sex;
public Employee(String name, int salary, String department, String sex) {
this.name = name;
this.salary = salary;
this.department = department;
this.sex = sex;
}
//getter and setters ..
}
Employee类包含name、salary、department和sex字段,并提供构造方法。
我们将从CSV文件emp_not_sorted.csv中读取员工数据创建Employee对象列表:
Sales,John Doe,48000,M
HR,Jane Smith,60000,F
IT,Robert Brown,75000,M
Marketing,Alice Johnson,55000,F
Sales,Chris Green,48000,M
HR,Emily White,62000,F
IT,Michael Black,72000,M
Marketing,Linda Blue,60000,F
更多记录...
CSV文件包含department、name、salary和sex四列。
读取CSV文件并存储到Map中:
static void populateMap(String filePath) throws IOException {
String[] lines = readLinesFromFile(filePath);
Arrays.asList(lines)
.forEach(e -> {
String[] strArr = e.split(",");
Employee emp = new Employee(strArr[1], Integer.valueOf(strArr[2]), strArr[0], strArr[3]);
MAP_OF_DEPT_TO_MAP_OF_SEX_TO_EMPLOYEES.computeIfAbsent(emp.getDepartment(),
k -> new HashMap<>())
.computeIfAbsent(emp.getSex(), k -> new ArrayList<>())
.add(emp);
});
}
关键点:MAP_OF_DEPT_TO_MAP_OF_SEX_TO_EMPLOYEES是Map<String, Map<String, List>>类型。外层Map的key是department,内层Map的key是sex。
下一步,我们将访问内层Map中的Employee列表,并按salary和name排序。期望的排序结果:
Sales,Chris Green,48000,M
Sales,John Doe,48000,M
Sales,Matthew Cyan,48000,M
Sales,David Grey,50000,M
Sales,James Purple,50000,M
Sales,Aiden White,55000,M
更多记录..
HR,Isabella Magenta,60000,F
HR,Jane Smith,60000,F
HR,Emily White,62000,F
HR,Sophia Red,62000,F
更多记录..
规则:先按salary升序,salary相同时按name升序。其他部门同理。
3. 非Stream API解决方案
传统方案*直接使用List#sort(Comparator)*方法**:
void givenHashMapContainingEmployeeList_whenSortWithoutStreamAPI_thenSort() throws IOException {
final List<Employee> lstOfEmployees = new ArrayList<>();
MAP_OF_DEPT_TO_MAP_OF_SEX_TO_EMPLOYEES.forEach((dept, deptToSexToEmps) ->
deptToSexToEmps.forEach((sex, emps) ->
{
emps.sort(Comparator.comparingInt(Employee::getSalary).thenComparing(Employee::getName));
emps.forEach(this::processFurther);
lstOfEmployees.addAll(emps);
})
);
String[] expectedArray = readLinesFromFile(getFilePath("emp_sorted.csv"));
String[] actualArray = getCSVDelimitedLines(lstOfEmployees);
assertArrayEquals(expectedArray, actualArray);
}
✅ 使用forEach()替代传统的for/while循环,这是Java 8引入的函数式编程特性。
✅ 通过Comparator#comparingInt()按salary排序,再链式调用thenComparing()按name排序,代码声明性强。
❌ 严重缺陷:sort()方法直接修改原始List(emps变量),破坏了不可变性原则。这种突变操作:
- 增加调试和排查问题的复杂度
- 无法返回新集合用于后续处理
- 需要额外循环处理元素,破坏代码流
4. Stream API解决方案
针对传统方案的缺陷,使用Stream API优雅解决:
void givenHashMapContainingEmployeeList_whenSortWithStreamAPI_thenSort() throws IOException {
final List<Employee> lstOfEmployees = new ArrayList<>();
MAP_OF_DEPT_TO_MAP_OF_SEX_TO_EMPLOYEES.forEach((dept, deptToSexToEmps) ->
deptToSexToEmps.forEach((sex, emps) ->
{
List<Employee> employees = emps.stream()
.sorted(Comparator.comparingInt(Employee::getSalary).thenComparing(Employee::getName))
.map(this::processFurther)
.collect(Collectors.toList());
lstOfEmployees.addAll(employees);
})
);
String[] expectedArray = readLinesFromFile(getFilePath("emp_sorted.csv"));
String[] actualArray = getCSVDelimitedLines(lstOfEmployees);
assertArrayEquals(expectedArray, actualArray);
}
✅ 核心优势:Stream#sorted(Comparator)返回新Stream对象,不修改原始数据。
✅ 操作链优势:
- 排序后可直接通过*map()处理每个元素(如调用processFurther()*)
- 支持多个中间操作(如filter/map等)
- 终止操作(*collect()*)生成最终结果
⚠️ 注意事项:Stream API需遵循函数式编程原则:
- 不可变性(Immutable)
- 无状态(Stateless)
- 纯函数(Pure Function) 否则可能产生意外结果。
5. 总结
本文对比了*Stream#sorted(Comparator)和List#sort(Comparator)*两种排序方案:
对比维度 | List#sort() | Stream#sorted() |
---|---|---|
原始数据修改 | ✅ 直接修改 | ❌ 保持不变 |
后续处理流畅度 | ❌ 需二次循环 | ✅ 链式操作 |
函数式编程支持 | ❌ 不支持 | ✅ 完全支持 |
代码可读性 | ⚠️ 一般 | ✅ 更清晰 |
结论:*Stream#sorted()*在代码连续性和可读性上明显优于传统方案。但使用Stream API时,务必遵循函数式编程原则,避免踩坑。
本文代码已发布在GitHub仓库。