likes
comments
collection
share

Java奇技淫巧:从单一入参到通用树形构建的进化之路

作者站长头像
站长
· 阅读数 3

Java奇技淫巧:从单一入参到通用树形构建的进化之路 我的博客:夜航猩-Space (cceven.cc)

序言

在开发过程中,树形结构是一种常见的数据结构,用于表示层级关系的数据。例如,组织结构、商品分类、菜单权限等数据经常需要以树形结构的形式展示和操作。在实际项目中,我们需要构建树形结构并进行操作,为了提高代码的复用性和可维护性,我们可以编写一个通用的方法来构建树形结构,并通过优化来提高其效率。

接下来就通过我对代码的一步步优化来实现如何从单一入参构建单一类型树形结构数据到通用树形构建的过程

业务背景

  1. 查询菜单列表,并构建树形结构然后返回给前端渲染
  2. 查询部门列表, 并构建树形结构然后返回给前端渲染
  • 以上两个业务场景应该是大家在做A端或B端应用时常见的使用场景,我一开始是如何实现的呢

一代目

// 构建部门树
 public static List<SysDepartmentVO> buildDepTree(List<SysDepartmentVO> source){
        if (CollUtil.isEmpty(source)) {
            return source;
        }
        return source.stream()
                .filter(dept -> Objects.equals(dept.getParentId(), NumberConstant.L_ZERO))
                .map(dept -> {
                    SysDepartmentVO root = new SysDepartmentVO();
                    BeanUtil.copyProperties(dept, root);
                    root.setChildren(getChildren(dept, source));
                    return root;
                })
                .collect(Collectors.toList());
    }
	
    // 构建菜单树
    public static List<SysMenuVO> buildMenuTree(List<SysMenuVO> menuList) {
        if (CollUtil.isEmpty(menuList)) {
            return menuList;
        }
        return menuList.stream()
                .filter(menu -> Objects.equals(menu.getParentId(), NumberConstant.L_ZERO))
                .map(menu -> {
                    SysMenuVO root = new SysMenuVO();
                    BeanUtil.copyProperties(menu, root);
                    root.setChildren(getChildren(menu, menuList));
                    return root;
                })
                .collect(Collectors.toList());
    }

 	// 查找 当前菜单下所有子菜单
    private static List<SysMenuVO> getChildren(SysMenuVO menu, List<SysMenuVO> menuList) {
        return menuList.stream()
                .filter(child -> child.getParentId().equals(menu.getId()))
                .map(child -> {
                    SysMenuVO childNode = new SysMenuVO();
                    BeanUtil.copyProperties(child, childNode);
                    // 此处递归
                    childNode.setChildren(getChildren(child, menuList));
                    return childNode;
                })
                .collect(Collectors.toList());
    }
	 // 查找 当前菜单下所有子菜单
    private static List<SysDepartmentVO> getChildren(SysDepartmentVO menu, List<SysDepartmentVO> menuList) {
        return menuList.stream()
                .filter(child -> child.getParentId().equals(menu.getId()))
                .map(child -> {
                    SysDepartmentVO childNode = new SysDepartmentVO();
                    // 此处递归
                    BeanUtil.copyProperties(child, childNode);
                    childNode.setChildren(getChildren(child, menuList));
                    return childNode;
                })
                .collect(Collectors.toList());
    }

分析

可以看到以上代码中包含两个主要方法 buildDepTreebuildMenuTree , 他们分别对各自的原始集合进行树结构封装, 细心的同学会发现, 他们除了入参集合中类型不同,其余代码均相同.虽然实现了对应的功能,但有着高度代码洁癖的我是不能容忍这样的代码出自于我手.

总结问题

经过刚才的分析,我们已经确定了这段代码的特点: 1, 相同的实现逻辑 2, 不同的入参类型

灵光一闪

此时脑海里你会想到什么? 没错就是 泛型

也有痛点

为什么说有痛点呢? 如果单纯使用泛型来实现,在抽象封装时不可避免的就要使用到Java反射机制.如果你要获取当前节点的 idparentId 来确定节点间子父级关系并且还需要获取 children 来对子节点进行赋值. 无形间增加了代码的复杂度,并且过度使用泛型会影响代码执行效率

确定方案

首先肯定使用泛型的,接下来就是考虑如何避免或不使用 反射 来达成目的, 你们想到了什么? 没错 接口或者抽象类啊! 于是 我定义一个树节点的接口 BaseNodeTree<T>,并定义了必要的方法,getId()getParentId()setChildren()等。然后,通过递归的方式构建树形结构,将每个节点的子节点递归添加到对应的父节点中。

二代目-实现

核心 BaseNodeTree


public interface BaseTreeNode<T> {

    /**
     * 获取id
     * @return 当前记录id
     */
    Long getId();

    /**
     * 获取父id
     *
     * @return 当前记录父id
     */
    Long getParentId();

    /**
     * 设置子节点
     * 
     * @param children  子节点集合
     */
    void setChildren(List<T> children);


}

对应实体类 集成 BaseNodeTree

伪代码, 仅做展示示例

public class SysMenuVO  implements BaseTreeNode<SysMenuVO> {

    private Long id;

    private Long parentId;

    private List<SysMenuVO> children;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public Long getParentId() {
        return parentId;
    }

    @Override
    public void setChildren(List<SysMenuVO> child) {
        children = child;
    }
}

构建树

// 构建树
public static <T extends BaseTreeNode<T>> List<T> buildTree(List<T> nodeList) {
        if (CollUtil.isEmpty(nodeList)) {
            return nodeList;
        }
        // 找父级
        return nodeList.stream()
                .filter(node -> Objects.equals(node.getParentId(), NumberConstant.L_ZERO))
                .map(node -> {
                    T root = BeanUtil.toBean(node, node.getClass());
                    // 找子集
                    root.setChildren(getChildren(node, nodeList));
                    return root;
                })
                .collect(Collectors.toList());
    }

	// 查询子集
    private static <T extends BaseTreeNode<T>> List<T> getChildren(T node, List<T> nodeList) {
        return nodeList.stream()
                .filter(child -> child.getParentId().equals(node.getId()))
                .map(child -> {
                    T childNode = BeanUtil.toBean(child, child.getClass());
                    childNode.setChildren(getChildren(child, nodeList));
                    return childNode;
                })
                .collect(Collectors.toList());
    }

小结

于是, 我们就抽象了一个公共的封装树形数据的方法, 妙妙妙! 到此, 可以说已经结束了, 但是代码看着还是有点长, 不太舒服

于是....

究极进化-完全体

废话不多说, 上代码

public static <T extends BaseTreeNode<T>> List<T> buildTree(List<T> nodeList) {
        if (CollUtil.isEmpty(nodeList)) {
            return nodeList;
        }
        // 以父id进行分组
        Map<Long, List<T>> map = nodeList.stream()
                .collect(Collectors.groupingBy(BaseTreeNode::getParentId));
        // 构建树
        return buildTreeNodes(map, NumberConstant.L_ZERO);
    }

    private static <T extends BaseTreeNode<T>> List<T> buildTreeNodes(Map<Long, List<T>> map, Long parentId) {
        List<T> nodes = map.get(parentId);
        if (nodes == null) {
            return new ArrayList<>();
        }
        return nodes.stream()
                .map(node -> {
                    @SuppressWarnings("unchecked")
                    T newNode = (T) BeanUtil.toBean(node, node.getClass());
                    newNode.setChildren(buildTreeNodes(map, node.getId()));
                    return newNode;
                })
                .toList();
    }

应用场景

这种通用的构建树形结构方法在实际项目中有着广泛的应用场景。例如,在电商系统中,可以用于构建商品分类树;在企业管理系统中,可以用于构建组织结构树;在权限管理系统中,可以用于构建菜单权限树等。通过这种方法,可以快速、灵活地构建和管理各种树形结构,提高系统的可扩展性和可维护性。

总结

通过构建通用的树形结构方法,我们可以提高代码的复用性和可维护性,同时通过优化提高效率。在实际项目中,可以根据具体需求对这个方法进行定制和扩展,以满足不同场景的需求。希望本文对您理解和使用树形结构方法有所帮助,欢迎交流讨论

版权申明

  • 文章作者: 夜航猩
  • 文章来源: 夜航猩-Space
  • 版权声明: 转载请附上原文出处链接及本声明。