Java案例如何实现树形结构?——完整代码与设计思路解析
目录导读
树形结构的核心问题:为什么需要它?
在实际开发中,我们常遇到需要展示层级关系的场景:比如电商网站的商品分类(服装 > 男装 > T恤)、公司组织架构(CEO > 部门经理 > 员工)、甚至后台权限菜单(系统管理 > 用户管理 > 角色管理),这些数据天然具有“父-子”关系,如果简单地用列表存储,查询某个节点下的所有子节点将需要多次数据库查询,性能极差。

树形结构通过维护每个节点的父节点ID或路径,让我们能在一次或少量数据库查询中获取完整层级,这是Java后端开发必须掌握的技巧。
基础实现方案:用Java对象构建树
1 核心实体类设计
最简单的树形节点需要以下字段:
public class TreeNode {
private Long id; // 节点唯一标识
private Long parentId; // 父节点ID(根节点为0或null)
private String name; // 节点名称
private List<TreeNode> children; // 子节点列表
// 构造函数、getter/setter省略
}
关键点:parentId 是链表的核心,它建立了节点间的父子关系。children 列表用于组装树时填充。
2 核心算法:列表转树
从数据库查询出所有节点(平铺列表)后,需要转换为树形结构,经典算法如下:
public List<TreeNode> buildTree(List<TreeNode> nodeList) {
// 1. 创建ID到节点的映射,便于快速查找
Map<Long, TreeNode> nodeMap = new HashMap<>();
for (TreeNode node : nodeList) {
nodeMap.put(node.getId(), node);
}
// 2. 组装父子关系
List<TreeNode> rootNodes = new ArrayList<>();
for (TreeNode node : nodeList) {
Long parentId = node.getParentId();
if (parentId == null || parentId == 0) {
rootNodes.add(node); // 根节点
} else {
TreeNode parent = nodeMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(node);
}
}
}
return rootNodes;
}
算法复杂度:O(n),仅需一次遍历即可完成,注意需要先构建Map,确保子节点能找到父节点对象。
数据库存储设计:如何高效查询树?
1 邻接表(Adjacency List)——最常用
数据库表设计:
| id | parent_id | name |
|---|---|---|
| 1 | NULL | 根目录 |
| 2 | 1 | 子分类 |
| 3 | 2 | 子子分类 |
优点:简单直观,支持无限层级。
缺点:查询某个节点的所有子节点需要递归查询(MySQL 8.0+ 支持递归CTE)。
MySQL递归查询示例:
WITH RECURSIVE cte AS (
SELECT id, parent_id, name, 1 AS level
FROM category WHERE id = 1
UNION ALL
SELECT c.id, c.parent_id, c.name, cte.level + 1
FROM category c
INNER JOIN cte ON c.parent_id = cte.id
)
SELECT * FROM cte;
2 路径枚举(Path Enumeration)——适合查询祖先
额外存储一个 path 字段,如 1/2/3 表示从根到当前节点的ID序列,查询某个节点所有子节点时,可用 LIKE '1/2/%',但修改节点路径时维护成本高。
3 闭包表(Closure Table)——适合大量读操作
额外创建一张表存储所有祖先-后代关系,虽然写操作复杂,但读操作极快。
选择建议:90%的场景使用邻接表 + 一次性加载所有节点到内存构建树即可(如在后台管理系统)。
案例实战:构建组织架构树(附完整代码)
1 需求描述
某公司需要展示组织架构树:CEO下分管CTO、CFO、COO,每个高管又管理各自的部门,前端需要JSON格式的树形数据。
2 完整Java实现
实体类(增加层级字段):
public class OrgNode {
private Long id;
private Long parentId;
private String name;
private Integer level; // 层级,根为0
private List<OrgNode> children;
// 省略getter/setter
}
Service层核心方法(使用MyBatis-Plus查询所有记录):
@Service
public class OrgService {
@Autowired
private OrgMapper orgMapper;
public List<OrgNode> buildOrgTree() {
// 1. 查询所有组织节点
List<OrgNode> allNodes = orgMapper.selectList(null);
// 2. 按层级排序,确保父节点先于子节点
allNodes.sort(Comparator.comparing(OrgNode::getLevel));
// 3. 构建树(复用之前的buildTree方法)
List<OrgNode> tree = buildTree(allNodes);
// 4. 对每个节点的子节点进行排序(如按名称)
sortChildren(tree);
return tree;
}
private void sortChildren(List<OrgNode> nodes) {
if (nodes == null || nodes.isEmpty()) return;
nodes.sort(Comparator.comparing(OrgNode::getName));
for (OrgNode node : nodes) {
sortChildren(node.getChildren());
}
}
}
Controller层暴露接口:
@RestController
@RequestMapping("/api/org")
public class OrgController {
@Autowired
private OrgService orgService;
@GetMapping("/tree")
public Result<?> getOrgTree() {
List<OrgNode> tree = orgService.buildOrgTree();
return Result.success(tree);
}
}
前端接收到的JSON示例:
[
{
"id": 1,
"parentId": null,
"name": "CEO",
"level": 0,
"children": [
{
"id": 2,
"parentId": 1,
"name": "CTO",
"level": 1,
"children": [
{
"id": 4,
"parentId": 2,
"name": "后端开发部",
"level": 2
}
]
},
{
"id": 3,
"parentId": 1,
"name": "CFO",
"level": 1
}
]
}
]
3 性能优化与注意事项
- 数据量过大:如果节点超过5000,建议使用递归查询+分页,避免一次性加载全部数据到内存。
- 循环引用:检查数据是否出现A的父节点是B,B的父节点是A的情况,否则会死循环,可以在组装树时设置最大深度(如10级)。
- 层级排序:在查询时按
parent_id排序,确保父节点先被处理,可简化builder逻辑。
常见问题解答(FAQ)
Q1:树形结构的层数有没有限制?
理论上没有,但实际开发建议限制在10层以内,过深的树会导致前端渲染卡顿,也不利于用户体验,如果需要更深的层级,考虑使用路径枚举存储。
Q2:如何删除一个节点及其所有子节点?
使用递归删除:先查出所有后代ID,再批量删除,在MySQL中可配合递归CTE实现:
WITH RECURSIVE cte AS (
SELECT id FROM category WHERE id = 5
UNION ALL
SELECT c.id FROM category c JOIN cte ON c.parent_id = cte.id
)
DELETE FROM category WHERE id IN (SELECT id FROM cte);
Q3:树形数据如何做排序?
在每个节点的children列表内按业务字段排序(如名称、权重、创建时间等),如果需要在全局排序(比如所有叶节点按名称排),建议在数据库层设计 sort_order 字段,查询后按此字段排序。
Q4:使用Spring Boot时,Jackson序列化会不会导致无限递归?
如果实体类中 children 字段包含父节点的引用(如 parent 字段),可能造成死循环,解决方案:
- 在
parent字段上添加@JsonIgnore注解。 - 使用DTO而不是实体类直接返回给前端。
Java构建树形结构的关键在于数据的建模(邻接表)和查询后的内存组装(Map映射+递归),掌握了以上案例,你可以轻松实现菜单、分类、组织架构等常见树形需求,实际项目中,记得考虑数据量、排序、循环引用等细节,如果对代码有任何疑问,欢迎在评论区讨论!