Java高效构建树形结构——异步加载子节点的实现方案
文章目录
- **一、改造节点模型**
- **二、定义异步加载接口**
- **三、改造树构建工具类**
- **四、实现按需加载逻辑**
- **五、使用示例**
- 1. 定义异步加载器(模拟数据库查询)
- 2. 构建初始树(仅根节点)
- 3. 前端触发加载某个节点的子节点
- **六、关键优化点**
- **七、适用场景**
Java高效构建树形结构的秘密:函数式编程 + 预排序的完美结合
要实现树形结构的异步加载子节点,核心思路是将子节点的获取过程延迟化,通过回调或 CompletableFuture
实现非阻塞加载。以下是具体实现步骤:
一、改造节点模型
在节点对象中增加两个字段,标识子节点是否已加载:
class TbMenuVo {
// 原有字段...
private volatile boolean childrenLoaded = false; // 子节点是否已加载
private volatile boolean loading = false; // 是否正在加载中(防重复请求)
}
二、定义异步加载接口
新增一个函数式接口,用于按需从外部获取子节点数据(例如从数据库或远程服务):
@FunctionalInterface
public interface AsyncChildrenLoader<T, ID> {
CompletableFuture<List<T>> loadChildren(ID parentId);
}
三、改造树构建工具类
修改 buildTree
方法,支持异步加载逻辑:
public static <T, ID> List<T> buildTreeAsync(
List<T> list,
Function<T, ID> getId,
Function<T, ID> getParentId,
BiConsumer<T, List<T>> setChildren,
Predicate<T> rootPredicate,
Comparator<T> comparator,
AsyncChildrenLoader<T, ID> childrenLoader // 新增异步加载器
) {
// 预构建父节点映射表(仅处理已知数据)
Map<ID, List<T>> parentMap = list.stream()
.filter(item -> getParentId.apply(item) != null)
.collect(Collectors.groupingBy(getParentId));
return list.stream()
.filter(rootPredicate)
.map(root -> {
// 初始化时仅设置直接子节点(若有)
ID rootId = getId.apply(root);
List<T> immediateChildren = parentMap.getOrDefault(rootId, Collections.emptyList());
setChildren.accept(root, immediateChildren);
return root;
})
.sorted(comparator)
.collect(Collectors.toList());
}
四、实现按需加载逻辑
为节点添加异步加载方法,触发时从 AsyncChildrenLoader
获取数据:
public static <T, ID> CompletableFuture<Void> loadChildrenAsync(
T node,
Function<T, ID> getId,
BiConsumer<T, List<T>> setChildren,
AsyncChildrenLoader<T, ID> childrenLoader,
Comparator<T> comparator
) {
ID nodeId = getId.apply(node);
if (node instanceof TbMenuVo) {
TbMenuVo menuNode = (TbMenuVo) node;
if (menuNode.isChildrenLoaded() || menuNode.isLoading()) {
return CompletableFuture.completedFuture(null);
}
menuNode.setLoading(true);
}
return childrenLoader.loadChildren(nodeId)
.thenApply(children -> {
// 对子节点排序
if (comparator != null) {
children.sort(comparator);
}
// 设置子节点并标记为已加载
setChildren.accept(node, children);
if (node instanceof TbMenuVo) {
TbMenuVo menuNode = (TbMenuVo) node;
menuNode.setChildrenLoaded(true);
menuNode.setLoading(false);
}
return null;
})
.exceptionally(ex -> {
// 处理异常(如记录日志)
if (node instanceof TbMenuVo) {
((TbMenuVo) node).setLoading(false);
}
return null;
});
}
五、使用示例
1. 定义异步加载器(模拟数据库查询)
AsyncChildrenLoader<TbMenuVo, Long> loader = parentId ->
CompletableFuture.supplyAsync(() -> {
// 模拟根据parentId查询数据库
List<TbMenuVo> children = database.queryChildrenByParentId(parentId);
return children;
});
2. 构建初始树(仅根节点)
List<TbMenuVo> tree = TreeBuilderOptimized.buildTreeAsync(
lists,
TbMenuVo::getId,
TbMenuVo::getParentId,
TbMenuVo::setChildren,
item -> item.getParentId() == 0,
Comparator.comparingInt(TbMenuVo::getSortOrder),
loader
);
3. 前端触发加载某个节点的子节点
// 当用户点击展开节点时触发
public void onExpandNode(TbMenuVo node) {
if (!node.isChildrenLoaded()) {
TreeBuilderOptimized.loadChildrenAsync(
node,
TbMenuVo::getId,
TbMenuVo::setChildren,
loader,
Comparator.comparingInt(TbMenuVo::getSortOrder)
).thenRun(() -> {
// 通知UI更新(例如前端重新渲染)
refreshUI();
});
}
}
六、关键优化点
- 防重复请求:通过
loading
标志位避免并发重复加载 - 线程安全:使用
volatile
确保状态可见性 - 异常处理:捕获异步加载中的异常并恢复状态
- 与现有工具兼容:保留同步构建方法,异步方法作为扩展
七、适用场景
- 大型树形结构数据(如万级以上节点)
- 需要动态加载的子节点(如文件系统目录树)
- 前端分批次渲染避免卡顿
通过以上改造,工具类既能保留原有高性能构建能力,又能灵活支持异步加载需求,实现“按需扩展”的树形数据处理!