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);
    }
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的 Java 工具类,可以将 List 转换为树形结构: ```java import java.util.*; public class TreeUtil { public static <T extends TreeNode> List<T> buildTree(List<T> nodes) { if (nodes == null || nodes.size() == 0) { return Collections.emptyList(); } Map<Long, T> nodeMap = new HashMap<>(); for (T node : nodes) { nodeMap.put(node.getId(), node); } List<T> rootNodes = new ArrayList<>(); for (T node : nodes) { T parent = nodeMap.get(node.getParentId()); if (parent != null) { parent.addChild(node); } else { rootNodes.add(node); } } return rootNodes; } public interface TreeNode { Long getId(); Long getParentId(); void addChild(TreeNode child); List<? extends TreeNode> getChildren(); } } ``` 这个工具类包含了一个通用的接口 `TreeNode`,通过实现这个接口,可以将任意类型的 List 转换为树形结构。 `TreeNode` 接口包含了三个方法: - `getId()`:获取节点的唯一标识符。 - `getParentId()`:获取节点的父节点标识符。 - `addChild(TreeNode child)`:将一个子节点添加到当前节点。 - `getChildren()`:获取当前节点的所有子节点。 使用这个工具类非常简单,只需要将需要转换的 List 传入 `buildTree()` 方法中即可: ```java List<MyNode> nodes = ...; // 获取需要转换的 List List<MyNode> rootNodes = TreeUtil.buildTree(nodes); ``` 其中 `MyNode` 是一个实现了 `TreeNode` 接口的自定义类。注意,为了能够正确地构建树形结构,每个节点的 `getParentId()` 方法必须返回其父节点的 `getId()` 值。否则,节点将无法正确地添加到树中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值