Java案例如何实现树形结构?

wen java案例 9

Java案例如何实现树形结构?——完整代码与设计思路解析

目录导读

  1. 树形结构的核心问题:为什么需要它?
  2. 基础实现方案:用Java对象构建树
  3. 数据库存储设计:如何高效查询树?
  4. 案例实战:构建组织架构树(附完整代码)
  5. 常见问题解答(FAQ)

树形结构的核心问题:为什么需要它?

在实际开发中,我们常遇到需要展示层级关系的场景:比如电商网站的商品分类(服装 > 男装 > T恤)、公司组织架构(CEO > 部门经理 > 员工)、甚至后台权限菜单(系统管理 > 用户管理 > 角色管理),这些数据天然具有“父-子”关系,如果简单地用列表存储,查询某个节点下的所有子节点将需要多次数据库查询,性能极差。

Java案例如何实现树形结构?

树形结构通过维护每个节点的父节点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映射+递归),掌握了以上案例,你可以轻松实现菜单、分类、组织架构等常见树形需求,实际项目中,记得考虑数据量、排序、循环引用等细节,如果对代码有任何疑问,欢迎在评论区讨论!

抱歉,评论功能暂时关闭!