📌 拿来即用 : Java 企业级工具类全家桶-44-树形结构构建工具类
📋 目录
一、工具类功能
把带父子关系的平铺列表一次组装成多级树,菜单、部门、地区、评论盖楼都走同一套入口,另外支持按主键在树里找节点、把树摊回前序列表。
典型用法是小表全量加载后组装成树直接返给前端,拿回来的是根节点列表、Optional 节点与摊平后的列表。类本身零第三方依赖,节点类只要有可读写的子集合字段(getter + setter)即可。
二、所需环境
JDK 17或以上
三、方法概览(按需调用)
| 序号 | 方法 | 入参 | 返回 | 用途 |
|---|---|---|---|---|
| 1 | buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter) | 平铺列表 + 主键取法 + 父键取法 + 子集合取法 + 子集合写法 | List | 自动判定根节点,一次组装多级树 |
| 2 | buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter, rootParentId) | 平铺列表 + 主键取法 + 父键取法 + 子集合取法 + 子集合写法 + 根节点父键 | List | 显式指定根节点的父键(如菜单表的 0) |
| 3 | findById(roots, targetId, idGetter, childrenGetter) | 根节点列表 + 目标主键 + 主键取法 + 子集合取法 | Optional | 在树里按主键找节点,未命中给空 Optional |
| 4 | flatten(roots, childrenGetter) | 根节点列表 + 子集合取法 | List | 把树摊回前序列表,父节点在子节点之前 |
四、调用示例(直观体验)
调用示例直接复制进 IDE 右键 Run 就能看到调用工具类各方法的输出结果。
java
21:00:31.858 [main] INFO -- 「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」返回根节点列表 -> [总部(1)[研发部(2)[服务端组(4), 前端组(5)], 市场部(3)[华东区(6)]]]
21:00:31.862 [main] INFO -- 「TreeBuildUtils 方法2 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter, rootParentId)」根父键给 0 时的根节点 -> [总部(1)[研发部(2)[服务端组(4), 前端组(5)], 市场部(3)[华东区(6)]]]
21:00:31.863 [main] INFO -- 「TreeBuildUtils 方法3 findById(roots, targetId, idGetter, childrenGetter)」命中节点 -> Optional[服务端组(4)]
21:00:31.863 [main] INFO -- 「TreeBuildUtils 方法3 findById(roots, targetId, idGetter, childrenGetter)」主键不存在 -> Optional.empty
21:00:31.865 [main] INFO -- 「TreeBuildUtils 方法4 flatten(roots, childrenGetter)」前序摊平的id序列 -> [1, 2, 4, 5, 3, 6]
21:00:31.866 [main] INFO -- 「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」父缺失时提升为根 -> [孤立菜单(10)[孤立的子菜单(11)]]
21:00:31.867 [main] INFO -- 「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」互相引用时裁一条边 -> [20],摊平后 -> [20, 21]
21:00:31.867 [main] ERROR -- 「异常信息获取演示」主键取法为null -> idGetter 不能为 null
21:00:31.868 [main] ERROR -- 「异常信息获取演示」子集合写法为null -> childrenSetter 不能为 null
21:00:31.868 [main] ERROR -- 「异常信息获取演示」子集合取法为null -> childrenGetter 不能为 null
进程已结束,退出代码为 0
java
package com.example.order.api;
import com.example.common.utils.TreeBuildUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* 调用示例:整个文件复制进 IDE,右键 Run 就能看效果。
*
* <p>建树工具只吃内存里的平铺列表,不依赖外部服务,也不需要 Spring 容器;节点类只要有可读写的
* 子集合字段(getter + setter)就能用,示例把菜单表组装成三级树,再演示查找、摊平与脏数据口径。
* 日志用 Lombok 的 {@code @Slf4j} + SLF4J 占位符,没装 Lombok 就把 {@code log} 换成项目自己的 Logger。
*/
@Slf4j
public class TreeBuildUtilsDemo {
/** 示例用的菜单节点,实际换成你自己的实体 */
public static class Menu {
private final Long id;
private final Long parentId;
private final String name;
private List<Menu> children = new ArrayList<>();
public Menu(Long id, Long parentId, String name) {
this.id = id;
this.parentId = parentId;
this.name = name;
}
public Long getId() {
return id;
}
public Long getParentId() {
return parentId;
}
public String getName() {
return name;
}
public List<Menu> getChildren() {
return children;
}
public void setChildren(List<Menu> children) {
this.children = children;
}
/** 打成「名称(主键)[子节点...]」,树的结构在日志里一眼可读 */
@Override
public String toString() {
return children.isEmpty() ? name + "(" + id + ")" : name + "(" + id + ")" + children;
}
}
public static void main(String[] args) {
// ========== 一、平铺列表自动判定根建树 ==========
List<Menu> roots = TreeBuildUtils.buildTree(menus(), Menu::getId, Menu::getParentId,
Menu::getChildren, Menu::setChildren);
log.info("「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」返回根节点列表 -> {}", roots);
// ========== 二、显式指定根父键 ==========
List<Menu> menuRoots = TreeBuildUtils.buildTree(menus(), Menu::getId, Menu::getParentId,
Menu::getChildren, Menu::setChildren, 0L);
log.info("「TreeBuildUtils 方法2 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter, rootParentId)」根父键给 0 时的根节点 -> {}",
menuRoots);
// ========== 三、树里按主键查找与摊平 ==========
Optional<Menu> hit = TreeBuildUtils.findById(roots, 4L, Menu::getId, Menu::getChildren);
log.info("「TreeBuildUtils 方法3 findById(roots, targetId, idGetter, childrenGetter)」命中节点 -> {}", hit);
Optional<Menu> miss = TreeBuildUtils.findById(roots, 99L, Menu::getId, Menu::getChildren);
log.info("「TreeBuildUtils 方法3 findById(roots, targetId, idGetter, childrenGetter)」主键不存在 -> {}", miss);
List<Menu> flattened = TreeBuildUtils.flatten(roots, Menu::getChildren);
log.info("「TreeBuildUtils 方法4 flatten(roots, childrenGetter)」前序摊平的id序列 -> {}", ids(flattened));
// ========== 四、脏数据与环的处理 ==========
// 父键 999 不在本批数据里:节点不丢,整条被提升为根
List<Menu> orphan = new ArrayList<>(List.of(new Menu(10L, 999L, "孤立菜单")));
orphan.add(new Menu(11L, 10L, "孤立的子菜单"));
List<Menu> orphanRoots = TreeBuildUtils.buildTree(orphan, Menu::getId, Menu::getParentId,
Menu::getChildren, Menu::setChildren);
log.info("「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」父缺失时提升为根 -> {}", orphanRoots);
// 两条记录互相引用:环上只裁一条父边,节点一个不少
List<Menu> cycle = new ArrayList<>(Arrays.asList(new Menu(20L, 21L, "环上A"),
new Menu(21L, 20L, "环上B")));
List<Menu> cycleRoots = TreeBuildUtils.buildTree(cycle, Menu::getId, Menu::getParentId,
Menu::getChildren, Menu::setChildren);
log.info("「TreeBuildUtils 方法1 buildTree(list, idGetter, parentIdGetter, childrenGetter, childrenSetter)」互相引用时裁一条边 -> {},摊平后 -> {}",
ids(cycleRoots), ids(TreeBuildUtils.flatten(cycleRoots, Menu::getChildren)));
// ========== 五、catch异常信息 ==========
// 1. 主键取法没给
try {
TreeBuildUtils.buildTree(menus(), null, Menu::getParentId, Menu::getChildren, Menu::setChildren);
} catch (NullPointerException e) {
log.error("「异常信息获取演示」主键取法为null -> {}", e.getMessage());
}
// 2. 子集合写法没给
try {
TreeBuildUtils.buildTree(menus(), Menu::getId, Menu::getParentId, Menu::getChildren, null);
} catch (NullPointerException e) {
log.error("「异常信息获取演示」子集合写法为null -> {}", e.getMessage());
}
// 3. 摊平时没给子集合取法
try {
TreeBuildUtils.flatten(roots, null);
} catch (NullPointerException e) {
log.error("「异常信息获取演示」子集合取法为null -> {}", e.getMessage());
}
}
/** 平铺的菜单表:根节点的父键是 0,同级顺序就是这里的书写顺序 */
private static List<Menu> menus() {
return new ArrayList<>(List.of(
new Menu(1L, 0L, "总部"),
new Menu(2L, 1L, "研发部"),
new Menu(3L, 1L, "市场部"),
new Menu(4L, 2L, "服务端组"),
new Menu(5L, 2L, "前端组"),
new Menu(6L, 3L, "华东区")));
}
private static List<Long> ids(List<Menu> nodes) {
return nodes.stream().map(Menu::getId).toList();
}
}
五、工具类源码(可直接复制)
📦 「点击此处可下载 Java企业级工具类全家桶-44-树形结构构建工具类」 的完整源码,拿来即用。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯