前言

项目开发中经常会遇到树形结构,如多级菜单、多级文件夹结构、多级分类结构、多级组织结构,这些结构都有个共同特点,就是一般存在数据库中是通过id和parentId保存父子级关系的,返回给前端需要合成一颗树,本文针对这类数据结构,总结出常用合成树的三种方法。

方法一:递归合成法

一般在会有一个Menu对象

@Data
public class Menu  {
    public Menu(Integer id, Integer parentId, String name, Integer weight) {
        this.id = id;
        this.parentId = parentId;
        this.name = name;
        this.weight = weight;
    }

    private Integer id;
    private Integer parentId;
    private String name;
    private Integer weight;
    private List<Menu> children;
}
    public static void main(String[] args)  {
        List<Menu> list = new ArrayList<>();
        list.add(new Menu(1, 0, "用户管理", 2));
        list.add(new Menu(2, 0, "租户管理", 1));
        list.add(new Menu(3, 1, "添加用户", 3));
        list.add(new Menu(4, 1, "删除用户", 2));
        list.add(new Menu(5, 2, "添加租户", 1));
        list.add(new Menu(6, 2, "删除租户", 2));
        
        List<Menu> tree1 = buildTree(list);
        System.out.println(JSONUtil.toJsonStr(tree1));
	}
	
    public static List<Menu> buildTree(List<Menu> list) {
        return list.stream()
                .filter(menu -> menu.getParentId() == 0)
                .peek(menu -> menu.setChildren(getChildrens(menu, list)))
                .sorted(Comparator.comparing(Menu::getWeight))
                .collect(Collectors.toList());
    }
    public static List<Menu> getChildrens(Menu root, List<Menu> allMenus) {
        return allMenus.stream()
                .filter(menu -> Objects.equals(menu.getParentId(), root.getId()))
                .peek(menu -> menu.setChildren(getChildrens(menu, allMenus)))
                .sorted(Comparator.comparing(Menu::getWeight))
                .collect(Collectors.toList());
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

方法二:使用泛型通用合成法

public interface TreeNode<T, E> extends Comparable<E> {
    T getId();
    T getParentId();
    T getWeight();
    boolean isRoot();
    void setChildren(List<? extends TreeNode<T, E>> children);
}

@Data
public class Menu implements TreeNode<Integer, Menu> {
    public Menu(Integer id, Integer parentId, String name, Integer weight) {
        this.id = id;
        this.parentId = parentId;
        this.name = name;
        this.weight = weight;
    }
    private Integer id;
    private Integer parentId;
    private String name;
    private Integer weight;
    private List<Menu> children;
    @Override
    public void setChildren(List menus) {
        this.children = menus;
    }
    @Override
    public boolean isRoot() {
        return this.parentId == 0;
    }
    @Override
    public int compareTo(Menu o) {
        return this.weight.compareTo(o.getWeight());
    }
}

    public static void main(String[] args) {
        List<Menu> list = new ArrayList<>();
        list.add(new Menu(1, 0, "用户管理", 2));
        list.add(new Menu(2, 0, "租户管理", 1));
        list.add(new Menu(3, 1, "添加用户", 3));
        list.add(new Menu(4, 1, "删除用户", 2));
        list.add(new Menu(5, 2, "添加租户", 1));
        list.add(new Menu(6, 2, "删除租户", 2));
        List<Menu> tree2 = tree(list);
        System.out.println(JSONUtil.toJsonStr(tree2));
    }
    public static <E extends TreeNode> List<E> getSubs(E parent, List<E> allData) {
        return allData.stream()
                .filter(x -> Objects.equals(x.getParentId(), parent.getId()))
                .peek(x -> x.setChildren(getSubs(x, allData)))
                .sorted()
                .collect(Collectors.toList());
    }
    public static <E extends TreeNode> List<E> tree(List<E> allData) {
        return allData.stream()
                .filter(TreeNode::isRoot)
                .peek(x -> x.setChildren(getSubs(x, allData)))
                .sorted()
                .collect(Collectors.toList());
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

方法三:使用Hutool 工具TreeUtill合成法

public static void main(String[] args) {
        List<Menu> list = new ArrayList<>();
        list.add(new Menu(1, 0, "用户管理", 2));
        list.add(new Menu(2, 0, "租户管理", 1));
        list.add(new Menu(3, 1, "添加用户", 3));
        list.add(new Menu(4, 1, "删除用户", 2));
        list.add(new Menu(5, 2, "添加租户", 1));
        list.add(new Menu(6, 2, "删除租户", 2));


        TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
        treeNodeConfig.setIdKey("id");
        List<Tree<Integer>> tree3 = TreeUtil.build(list, 0, treeNodeConfig, (node, tree) -> {
            tree.setId(node.getId());
            tree.setParentId(node.getParentId());
            tree.setName(node.getName());
            tree.setWeight(node.getWeight());
        });
        System.out.println(JSONUtil.toJsonStr(tree3));
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

方法四:使用Lambda 表达式改写泛型接口

public static void main(String[] args) {
        List<Menu> list = new ArrayList<>();
        list.add(new Menu(1, 0, "用户管理", 2));
        list.add(new Menu(2, 0, "租户管理", 1));
        list.add(new Menu(3, 1, "添加用户", 3));
        list.add(new Menu(4, 1, "删除用户", 2));
        list.add(new Menu(5, 2, "添加租户", 1));
        list.add(new Menu(6, 2, "删除租户", 2));
        List<Menu> menus= makeTree(list,x-> x.getParentId()==0,(x,y)-> x.getId().equals(y.getParentId()), Menu::setChildren);
        System.out.println(JSONUtil.toJsonStr(menus));
    }

    public static <E> List<E> makeTree(List<E> list, Predicate<E> root, BiFunction<E,E,Boolean> parentCheck, BiConsumer<E,List<E>> children) {
        return list.stream().filter(root).peek(x->children.accept(x,makeChildren(x,list, parentCheck,children))).collect(Collectors.toList());
    }
    public static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E,E,Boolean> parentCheck, BiConsumer<E,List<E>> children) {
        return allData.stream().filter(x-> parentCheck.apply(parent,x)).peek(x->children.accept(x,makeChildren(x,allData, parentCheck,children))).collect(Collectors.toList());
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

总结

  1. 方法一:
    优点:逻辑简单清晰
    缺点:代码不能复用
  2. 方法二:
    优点:代码可以复用
    缺点:需要实现接口,代码侵入性太强
  3. 方法三:
    优点:无代码侵入,可复用
    缺点:需要引用Hutool包,返回对象变成Hutoo的Tree
  4. 方法四:
    优点:无代码侵入,代码可复用,不需要引用额外包
    缺点:不支持JDK1.8以下版本