💡 Java Stream 高级应用:优雅地扁平化(FlatMap)递归树形结构数据

在 Java 开发中,处理具有层级关系的 树形结构数据 是常见的需求,例如组织架构、文件目录或多级菜单。如何将这种带有 children
列表的递归结构完全扁平化(Flatten) ,并用现代化的 Java Stream API 进行处理,是一个高效且优雅的解决方案。
本文将通过一个清晰的 "部门(Department)" 示例,演示如何利用 flatMap
和匿名递归函数, 在纯 Java Stream 中实现复杂的树形结构扁平化。
🏢 示例结构:部门(Department)类
首先,定义我们的树形结构类 Department
:
java
import java.util.List;
public class Department {
private final long id;
private final String name;
/** 子部门列表 */
private List<Department> children;
// 构造器和Getter...
public long getId() { return id; }
public List<Department> getChildren() { return children; }
// ...
}
我们的目标是:
给定一个顶级部门列表
List<Department>
,最终获取
所有部门(包括所有子孙) 的 ID 列表List<Long>
。
🧠 核心技巧:Stream 内部的递归 FlatMap
在 Stream 中,flatMap
用于将 Stream 中的元素转换为新的 Stream
并连接起来。要实现递归扁平化,关键在于:
- 利用 匿名内部类 (
new Function<...>()
) 来实现java.util.function.Function
接口; - 在
apply
方法内部,通过this::apply
实现对自身的递归调用; - 使用
Stream.concat()
将当前节点和所有子孙节点的 Stream 拼接起来。
我们将展示两种基于此核心技巧的实现方式。
✅ 写法一:使用局部 Function 变量(推荐)
这种方式将递归逻辑提取到一个局部变量中,使主 Stream 流程保持简洁清晰。
java
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// 假设这是我们的顶级部门列表
List<Department> topLevelDepartments = createSampleDepartments();
// 1️⃣ 定义一个局部匿名 Function 变量,用于实现递归扁平化
Function<Department, Stream<Department>> flatMapRecursive =
new Function<Department, Stream<Department>>() {
@Override
public Stream<Department> apply(Department dept) {
// 自身的 Stream
Stream<Department> selfStream = Stream.of(dept);
// 子集的 Stream:递归调用 apply 并 flatMap 展平
Stream<Department> childrenStream =
(dept.getChildren() != null && !dept.getChildren().isEmpty())
? dept.getChildren().stream().flatMap(this::apply) // ★ 递归自调用
: Stream.empty();
// 合并自身和子集的 Stream
return Stream.concat(selfStream, childrenStream);
}
};
// 2️⃣ Stream 主流程:扁平化并提取 ID
List<Long> allDepartmentIds = topLevelDepartments.stream()
// 使用定义的递归函数进行扁平化
.flatMap(flatMapRecursive)
// 3️⃣ 提取所有部门的 ID
.map(Department::getId)
// 4️⃣ 收集结果
.collect(Collectors.toList());
优点:
- ✅ 可读性高:主 Stream 流程非常简洁;
- ✅结构清晰:递归逻辑被封装在一个命名变量中;
- ✅实用性强:推荐在日常开发中使用。
✨ 写法二:直接嵌入 flatMap 匿名类
这种方式将递归逻辑直接作为参数嵌入到 flatMap
中,实现了"完全在一个流里解决"的效果。
java
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// 假设这是我们的顶级部门列表
List<Department> topLevelDepartments = createSampleDepartments();
List<Long> allDepartmentIds = topLevelDepartments.stream()
// 关键步骤:直接在 flatMap 中定义并实现递归函数
.flatMap(new Function<Department, Stream<Department>>() {
@Override
public Stream<Department> apply(Department dept) {
Stream<Department> selfStream = Stream.of(dept);
Stream<Department> childrenStream =
(dept.getChildren() != null && !dept.getChildren().isEmpty())
? dept.getChildren().stream().flatMap(this::apply) // ★ 递归自调用
: Stream.empty();
return Stream.concat(selfStream, childrenStream);
}
})
// 提取所有部门的 ID
.map(Department::getId)
// 收集结果
.collect(Collectors.toList());
优点:
- ✅ 代码紧凑:避免了额外的局部变量定义;
- ✅ 满足纯 Stream要求:实现了"一条链解决所有问题";
缺点:
- ⚠️ 可读性较低:flatMap 内部代码块较长;
- ⚠️逻辑嵌套深,不易复用。
📊 总结对比
写法一:局部变量 Function | 写法二:嵌入匿名类 | |
---|---|---|
主流程可读性 | ✅ 高(推荐) | ❌ 低(代码块长) |
递归逻辑封装 | ✅ 独立命名变量 | ❌ 内嵌逻辑,不易重用 |
可维护性 | ✅ 强 | ⚠️ 弱 |
应用场景 | 推荐在所有项目中使用 | 适用于行数要求严格的场景 |
🏁 总结
无论采用哪种写法,核心思路都是通过:
匿名内部类 +
Stream.concat()
+this::apply
实现递归扁平化
这种方式不仅优雅,而且能充分利用 Stream 的惰性特性与函数式编程风格。
✅ 推荐实践:
在日常开发中,建议使用 "写法一" 既优雅又清晰,是现代 Java 处理树形结构的最佳实践之一。