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);
}