Java工具类之TreeUtils,构建树形数据结构

Java工具类之TreeUtils,构建树形数据结构

在实际项目开发过程中,我们可能会遇到需要将数据以树形结构的形式进行展示,比如:权限数、菜单树等等,虽然现在很多前端工具类可以自行构建(比如treegrid等),但为了方便,完全可以在后端进行数据的树形结构构建。

注意,本人目前使用的JDK版本为21,但理论上JDK11及以上版本都应该是适配的,如果移植时有错误,根据自己JDK版本修改特性进行适配。

树形结构标识

TreeNode用来标识对象支持树形结构,即如需要使用工具类进行树形结构的构建时,需要实现此接口。

package demo;

import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * 树形节点标识,同时定义树形结果中节点的基本行为。
 * <p>
 * 请注意,树形结果的实现类必须实现该接口,同时需要添加{@link Comparable#compareTo(T)}方法,用于排序<p/>
 * 根据自己的实际业务需求,自行删除或实现排序方法
 *
 * @param <T> 节点类型,实现该接口的实现类
 * @param <ID> 节点ID类型
 * @author KVSonen
 */
public interface TreeNode<T, ID> extends Comparable<T> {

    /**
     * 获取当前节点的ID,此ID值一般推荐使用数据库中的ID。<p/>
     * 这里的ID值为了通用性,定义为泛型,根据实际业务需求注入自己的ID类型。
     *
     * @return 当前节点的ID值
     */
    ID getId();

    /**
     * 获取当前节点的子节点集合,节点和子节点根据id和parentId进行关联
     *
     * @return 当前节点的子节点集合
     * @see TreeNode#getId()
     * @see TreeNode#getParentId()
     */
    List<T> getChildren();

    /**
     * 向当前节点添加一个子节点,此处添加按照逐一添加的方式。根据实际业务需要,实现类可以拥有自己setChildren()方法。<p/>
     * 但还是推荐实现类都实现该方法,因为在{@link demo.TreeUtils#buildTree(List)}中构建树形结构
     * 时,是通过该方法进行子节点的构建。
     *
     * @param child 需要添加的子节点
     */
    void addChild(T child);

    /**
     * 获取当前节点的父节点ID。默认实现返回null,表示没有父节点。
     *
     * @return 父节点的ID值,如果没有父节点则返回null。
     */
    default ID getParentId() {
        return null;
    }

    /**
     * 判断当前节点是否拥有子节点。该方法主要用于前端展示的判断依据。
     *
     * @return 是否拥有子节点,如果当前节点拥有子节点,则返回true,否则返回false。
     */
    @SuppressWarnings("unused")
    default boolean hasChild() {
        return !CollectionUtils.isEmpty(getChildren());
    }
}

TreeUtils

TreeUtils用来生成树形结构,数据节点必须实现上面的标识接口TreeNode。

package demo;

import demo.TreeNode;
import jakarta.annotation.Nonnull;

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

/**
 * TreeUtils类提供了树形结构的构建方法,当一个列表数据需要转换成树形结构时,可以使用该类提供的方法。
 * <p>
 * 但列表类型必须实现接口{@link TreeNode},在接口中定义了父子结构的关联属性,此处构建则是依据该关联属性进行构建。
 *
 * @author KVSonen
 * @see TreeNode
 */
public final class TreeUtils {

    /**
     * 根据给定的列表构建树形结构,列表不能为空
     *
     * @param list 所有节点的集合,节点必须实现接口{@link TreeNode}
     * @param <T>  节点类型,接口{@link TreeNode}的实现类
     * @return 树形结构,在返回列表中,根节点为所有parentId为null的节点
     * @see TreeNode
     */
    public static <T extends TreeNode<T, ?>> List<T> buildTree(@Nonnull List<T> list) {

        // 过滤出所有没有父节点的节点作为根节点
        // 此处结果默认是经过排序的列表,排序方法为接口 TreeNode 实现类的compareTo方法
        var root  = list.stream()
                .filter(node -> node.getParentId() == null)
                .sorted()
                .toList();
        var tempList = new ArrayList<>(list);
        // 减少遍历次数,在构建过程中将已选取的节点从list中移除
        tempList.removeAll(root);
        // 构建children
        root.forEach(node -> {
            // 构建子节点
            buildChild(tempList, node);
        });
        return root;
    }

    /**
     * 为给定节点的节点构建子节点
     *
     * @param list 所有节点的集合,节点必须实现接口{@link TreeNode}
     * @param node 当前正在构建子树的节点。
     * @param <T>  节点类型,接口{@link TreeNode}的实现类
     */
    private static <T extends TreeNode<T, ?>> void buildChild(@Nonnull List<T> list, @Nonnull T node) {
        // 找到当前节点的所有子节点
        // 此处结果默认是经过排序的列表,排序方法为接口 TreeNode 的实现类的compareTo方法
        var children = list.stream()
                .filter(child -> child.getParentId() != null && child.getParentId().equals(node.getId()))
                .sorted()
                .toList();
        // 减少遍历次数,在构建过程中将已选取的节点从list中移除
        var tempList = new ArrayList<>(list);
        tempList.removeAll(children);
        // 构建children的children
        children.forEach(child -> {
            // 递归构建子节点的子树
            buildChild(tempList, child);
            // 将当前节点添加进子节点列表中
            node.addChild(child);
        });
    }
}

使用示例:TreeNode实现类

package demo;

import demo.PermissionPO;
import demo.TreeNode;
import jakarta.annotation.Nonnull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 系统菜单VO,用以反馈页面进行展示。
 * 支持树形结果,可序列化
 *
 * @author KVSonen
 * @see TreeNode
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MenuVO implements TreeNode<MenuVO, Integer>, Serializable {
    @Serial
    private static final long serialVersionUID = 609910470679455084L;

    // 菜单的唯一标识符
    private Integer id;

    // 菜单名称
    private String name;

    // 菜单URL,当拥有子菜单时,URL可以为空
    private String url;

    // 菜单图标
    private String icon;

    // 父菜单的标识符
    private Integer parentId;

    // 菜单的排序值,值越小,越靠前显示
    private Integer sort;

    // 该菜单下的子菜单列表,支持所限制层级的菜单
    // 但目前前端只实现了2层菜单
    private List<MenuVO> children;

    /**
     * 构造函数,从权限实体PO对象转换为菜单VO对象
     *
     * @param permissionPO 来源于数据库的权限实体对象
     */
    public MenuVO(@Nonnull PermissionPO permissionPO) {
        this.id = permissionPO.getId();
        this.name = permissionPO.getName();
        this.url = permissionPO.getUrl();
        this.icon = permissionPO.getIcon();
        this.parentId = permissionPO.getParentId();
        this.sort = permissionPO.getSort();
    }


    /**
     * 添加子菜单
     *
     * @param child 要添加的子菜单VO对象
     */
    @Override
    public void addChild(@Nonnull MenuVO child) {
        if (children == null) {
            children = new ArrayList<>();
        }
        children.add(child);
    }

    /**
     * 实现Tree接口的compareTo方法,用于菜单的排序
     *
     * @param other 另一个菜单VO对象
     * @return 返回基于sort属性的比较结果
     */
    @Override
    public int compareTo(@Nonnull MenuVO other) {
        return this.sort == null || other.sort == null ? 0 : this.sort - other.sort;
    }
}

使用方法:树形结构构建

    public List<MenuVO> getUserMenus() {
        var userInfo = userRepository.findById(1);
        var menus = userInfo.orElseGet(UserPO::new).getRoles().stream()
                .filter(RolePO::isEnable)
                .filter(role -> !role.isDeleted())
                .map(RolePO::getPermissions)
                .flatMap(List::stream)
                .filter(PermissionPO::isEnable)
                .filter(permissionPO -> !permissionPO.isDeleted())
                .filter(permissionPO -> permissionPO.getType() == PermissionType.MENU)
                .map(MenuVO::new)
                .distinct()
                .toList();
        return TreeUtils.buildTree(menus);
    }
$(function(){ $.fn.extend({ SimpleTree:function(options){ //初始化参数 var option = $.extend({ click:function(a){ } },options); option.tree=this; /* 在参数对象中添加对当前菜单的引用,以便在对象中使用该菜单 */ option._init=function(){ /* * 初始化菜单展开状态,以及分叉节点的样式 */ this.tree.find("ul ul").hide(); /* 隐藏所有子级菜单 */ this.tree.find("ul ul").prev("li").removeClass("open"); /* 移除所有子级菜单父节点的 open 样式 */ this.tree.find("ul ul[show='true']").show(); /* 显示 show 属性为 true 的子级菜单 */ this.tree.find("ul ul[show='true']").prev("li").addClass("open"); /* 添加 show 属性为 true 的子级菜单父节点的 open 样式 */ }/* option._init() End */ /* 设置所有超链接不响应单击事件 */ this.find("a").click(function(){ $(this).parent("li").click(); return false; }); /* 菜单项 接受单击 */ this.find("li").click(function(){ /* * 当单击菜单项 * 1.触发用户自定义的单击事件,将该 标签中的第一个超链接做为参数传递过去 * 2.修改当前菜单项所属的子菜单的显示状态(如果等于 true 将其设置为 false,否则将其设置为 true) * 3.重新初始化菜单 */ option.click($(this).find("a")[0]); /* 触发单击 */ /* * 如果当前节点下面包含子菜单,并且其 show 属性的值为 true,则修改其 show 属性为 false * 否则修改其 show 属性为 true */ /* if($(this).next("ul").attr("show")=="true"){ $(this).next("ul").attr("show","false"); }else{ $(this).next("ul").attr("show","true"); }*/ /* 初始化菜单 */ option._init(); }); /* 设置所有父节点样式 */ this.find("ul").prev("li").addClass("folder"); /* 设置节点“是否包含子节点”属性 */ this.find("li").find("a").attr("hasChild",false); this.find("ul").prev("li").find("a").attr("hasChild",true); /* 初始化菜单 */ option._init(); }/* SimpleTree Function End */ }); });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值