1. 引言

工作中经常会碰到需要建立目录,菜单,角色等相关的树形结构,这种情况下少不了用到递归(recursion),但这种方式很容易造成StackOverflow 即堆栈溢出的问题;

2.java代码实现

  • 树形结构关键pojo
@Setter     //这里不能用@Data注解,@Data注解包含了(getter/setter、equals()、hashCode()、toString())      
@Getter     //而Lombok 工具中@Data注解生成hashCode()不适合,会导致StackOverflowError = null的异常      
@NoArgsConstructor
@AllArgsConstructor
public class CategoryTree implements Serializable {

    private Long id;
    private Long pid;
    private String name;
    private String iconUrl;
    private Boolean leaf = false;
    private List<CategoryTree> children;

    public CategoryTree(Long id,Long pid,String name){
        this.id = id;
        this.pid = pid;
        this.name = name;
    }
}
  • 下面是分类树中的核心实现方法
package com.smart.service.impl;

import com.smart.pojo.Category;
import com.smart.util.IdWorker;
import com.smart.vo.CategoryTree;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@EnableScheduling
@Service
public class CategoryServiceImpl implements CommandLineRunner {
    //注意这里的voltile 关键字
    private static volatile CategoryTree categoryTree;

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public void run(String... args){
        log.info("\n------------------------------------------------------\n" +
                "\t启动程序后初始化构建分类树\n" +
                "\n------------------------------------------------------\n");
        buildCategoryTree();
    }

    @Scheduled(cron = "${catrgoryTree.buildCycle:0 10 0 * * ? }")
    private void buildCategoryTree() {
        log.info("\n------------------------------------------------------\n" +
                "\t开始构建分类关系树构\n" +
                "\n------------------------------------------------------\n");
        long startTime1 = System.currentTimeMillis();
        List<Category> categoryList = this.listAllCategory();
        categoryTree = buildCategoryListToTree(categoryList,0L);
        log.info("\n------------------------------------------------------\n" +
                        "\t原理图分类树构建完毕,共构建{}个分类数据,耗时:{} ms\n" +
                        "\n------------------------------------------------------\n",
                categoryList.size(),(System.currentTimeMillis() - startTime1));
    }

    public CategoryTree getCategory() {
        return categoryTree;
    }

    public CategoryTree buildCategoryListToTree(final List<Category> categoryList, final Long rootId) {
        final List<CategoryTree> categoryTrees = new ArrayList<>(categoryList.size());
        if (categoryList != null && !categoryList.isEmpty()) {
            for (Category category : categoryList) {
                categoryTrees.add(bulidCategoryTree(category));
            }
        }
        final CategoryTree root = findCategoryTreeRoots(categoryTrees, rootId);
        // 移除根节点
        categoryTrees.remove(root);
        root.setChildren(findDeptTreeChildren(root, categoryTrees));
        return root;
    }
    //查询出所有的分类表中的数据
    public List<Category> listAllCategory() {
        return categoryMapper.listAllCategory();
    }
    //找到分类树中的root节点
    public CategoryTree findCategoryTreeRoots(final List<CategoryTree> allNodes, final Long rootId) {
        final CategoryTree result = new CategoryTree();
        for (CategoryTree node : allNodes) {
            if(node.getId().equals(rootId)) {
                result.setId(rootId);
                result.setPid(node.getPid());
                result.setName(node.getName());
            }
        }
        return result;
    }

    private CategoryTree bulidCategoryTree(final Category category) {
        return new CategoryTree(category.getId(),category.getPid(),category.getName());
    }

    private List<CategoryTree> findDeptTreeChildren(final CategoryTree root, final List<CategoryTree> allNodes) {
        final List<CategoryTree> children = new ArrayList<>();
        for (CategoryTree comparedOne : allNodes) {
            if(null == comparedOne || null == comparedOne.getPid()) {
                continue;
            }
            if (comparedOne.getPid().equals(root.getId())) {
                children.add(comparedOne);
            }
        }
        final List<CategoryTree> notChildren = (List<CategoryTree>) CollectionUtils.subtract(allNodes, children);
        for (CategoryTree child : children) {
            final List<CategoryTree> tmpChildren = findDeptTreeChildren(child, notChildren);
            if (tmpChildren == null || tmpChildren.isEmpty()) {
                child.setLeaf(true); //设置是否是叶子节点的标志
            } else {
                child.setLeaf(false);
            }
            child.setChildren(tmpChildren);
        }
        return children;
    }
}

注意:

  1. 实现了CommandLineRunner接口,会在springboot项目启动后初始执行一次,并在内存中创建出树形结构
  2. 其中加入了@EnableScheduling注解,是为了定时去刷新整个树形结构,当然也可以定义接口,在目录发生了变化后来手动刷新,根据实际情况来选择;
  3. 以上的两点根据情况结合自己项目实际来决定,下面简单讲一下关键要点
  4. volatile
//注意这里的voltile 关键字
    private static volatile CategoryTree categoryTree;
  1. CollectionUtils.subtract方法
    这里导入的是 import org.apache.commons.collections4.CollectionUtils;
    作用是从大的集合中减去小的集合,在这里是为了除去已经加入树形结构的数据,将剩余的节点继续进行递归
final List<CategoryTree> notChildren = (List<CategoryTree>) CollectionUtils.subtract(allNodes, children);
  1. 叶子节点的标志
child.setLeaf(true); //设置是否是叶子节点的标志

3. 实际使用过程出现的问题

  1. 原本我在核心的pojo类中还加入了一个private CategoryTree parent;(箭头所指的地方);
    原本考虑的是方便子节点去向上直接找到父节点,但这是不行的,会报StackOverflow=null的异常,因为这里子节点通过指针指向父节点,再加一个父节点指向子节点的指针,我个人的理解是会陷入一种循环中,对象的判断是通过hashCode方法区分的,实际也不需要加入这个父节点,通过父id去找具体节点也一样。
@Setter     //这里不能用@Data注解,@Data注解包含了(getter/setter、equals()、hashCode()、toString())      
@Getter     //而Lombok 工具中@Data注解生成hashCode()不适合,会导致StackOverflowError = null的异常      
@NoArgsConstructor
@AllArgsConstructor
public class CategoryTree implements Serializable {

    private Long id;
    private Long pid;
    private String name;
    private String iconUrl;
    private Boolean leaf = false;
    private CategoryTree parent;       <-------------------------------------------
    private List<CategoryTree> children;

    public CategoryTree(Long id,Long pid,String name){
        this.id = id;
        this.pid = pid;
        this.name = name;
    }
}
  1. 通过getCategory()方法获取到tree,实际使用中会围绕这个tree做很多节点的增删查改的操作,具体大家根据实际情况自行拓展;
  2. 递归很容易造成StackOverflow问题,那么有没有什么方式将递归方式转化为非递归方法,答案是肯定的,这里我贴上我找到的一个介绍得很清楚的帖子漫谈递归转非递归

4.结尾

最后以一个递归美图结束这无休止的文章编写!

java 树结构 递归算法 java树形结构递归实现_子节点