引言
在软件开发中,我们经常需要处理具有层级关系的数据,如组织架构、评论系统、分类目录等。这些数据通常以树形结构来表示,其中每个节点可以有零个或多个子节点。然而,数据库中的层级数据往往是以扁平表的形式存储的,这就需要我们在应用层将这些数据转换成树形结构。为了解决这个问题,我编写了一个通用的Java方法,可以自动将扁平数据转换成树形结构。
方法介绍
下面,我将详细介绍这个方法,它位于一个名为BuildTree
的类中,方法名为buildTree
。这个方法接受四个参数:
list
:包含所有节点的扁平列表。idFieldName
:节点ID的字段名。pidFieldName
:父节点ID的字段名。childrenFieldName
:子节点列表的字段名。
方法的返回类型是List<T>
,表示根节点的列表。
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName buildTree
* @Description
* @Author @rwq
* @Date 2024/12/16 17:12 星期一
* @Version 1.0
*/
public class BuildTree {
public static <T> List<T> buildTree(List<T> list, String idFieldName, String pidFieldName, String childrenFieldName)
throws NoSuchFieldException, IllegalAccessException {
Map<String, T> nodeMap = new HashMap<>();
List<T> rootNodes = new ArrayList<>();
// 将所有节点放入Map中,key为节点的id
for (T node : list) {
Field idField = node.getClass().getDeclaredField(idFieldName);
idField.setAccessible(true);
String id = (String) idField.get(node);
nodeMap.put(id, node);
}
// 构建树结构
for (T node : list) {
Field pidField = node.getClass().getDeclaredField(pidFieldName);
pidField.setAccessible(true);
String pid = (String) pidField.get(node);
T parentNode = nodeMap.get(pid);
if (parentNode == null) {
// 没有父节点,作为根节点
rootNodes.add(node);
} else {
// 获取父节点的children字段
Field childrenField = parentNode.getClass().getDeclaredField(childrenFieldName);
childrenField.setAccessible(true);
// 获取父节点的children列表,如果不存在则初始化
@SuppressWarnings("unchecked")
List<T> children = (List<T>) childrenField.get(parentNode);
if (children == null) {
children = new ArrayList<>();
childrenField.set(parentNode, children);
}
// 将当前节点添加到父节点的children列表中
children.add(node);
}
}
return rootNodes;
}
}
方法实现详解
-
节点映射:首先,我们创建一个
HashMap
,用于存储所有节点,其中key是节点的ID,value是节点对象本身。这样,我们可以快速通过ID查找节点。 -
构建树结构:然后,我们遍历扁平列表中的每个节点。对于每个节点,我们查找其父节点(通过父节点ID在映射中查找)。如果父节点不存在,说明该节点是根节点,我们将其添加到根节点列表中。如果父节点存在,我们获取父节点的子节点列表字段,如果不存在则初始化一个空列表,并将当前节点添加到该列表中。
-
返回根节点列表:最后,我们返回根节点列表,这些根节点及其子节点共同构成了完整的树形结构。
使用示例
假设我们有一个CommentsTreeVo
类,表示评论树的节点,其中包含id
、parentId
和children
字段。
import com.baomidou.mybatisplus.annotation.*;
import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType;
import com.github.appundefined.tree.TreeElement;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import vip.xiaonuo.common.pojo.CommonEntity;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 评论实体
*
* @author rwq
* @date 2024/12/13 07:59
**/
@Getter
@Setter
@ToString
public class CommentsTreeVo {
/** 评论id */
@Schema(description = "评论id")
private String id;
//业务字段
/** 收藏的用户id,(外键el_user用户表) */
@Schema(description = "收藏的用户id,(外键el_user用户表)")
private String userId;
//业务字段
/** 收藏的动态id(外键动态表) */
@Schema(description = "收藏的动态id(外键动态表)")
private String postId;
/** 父评论ID,NULL表示顶级评论 */
@Schema(description = "父评论ID,NULL表示顶级评论")
private String parentId;
//业务字段
@Schema(description = "内容")
private String content;
private List<CommentsTreeVo> children = new ArrayList<>();
//以下都是业务字段
/** 创建时间(评论时间) */
@Schema(description = "创建时间(评论时间)")
private Date createTime;
/** 删除标志 */
@Schema(description = "删除标志")
private String deleteFlag;
/** 创建人 */
@Schema(description = "创建人")
private String createUser;
/** 创建人名称 */
@Schema(description = "创建人名称")
private String createUserName;
/** 更新时间 */
@Schema(description = "更新时间")
private Date updateTime;
/** 更新人 */
@Schema(description = "更新人")
private String updateUser;
/** 更新人名称 */
@Schema(description = "更新人名称")
private String updateUserName;
}
我们可以使用buildTree
方法将评论列表转换成评论树:
List<CommentsTreeVo> comments = // 从数据库获取的评论列表(待处理的数据)
List<CommentsTreeVo> tree = BuildTree.buildTree(comments, "id", "parentId", "children");
这样,tree
就包含了转换后的评论树形结构,我们可以轻松地进行后续操作,如遍历、展示等。
注意事项
-
字段访问:由于
buildTree
方法使用了反射来访问对象的字段,因此可能会抛出NoSuchFieldException
和IllegalAccessException
。在实际使用中,我们需要确保传入的字段名在节点类中确实存在,并且具有相应的访问权限。 -
性能考虑:反射操作相对较慢,如果数据量大或者需要频繁进行树形结构转换,可以考虑使用其他更高效的方案,如手动编写转换逻辑或使用专门的库。
-
泛型限制:
buildTree
方法使用了泛型,可以处理任意类型的节点对象。但是,这也意味着我们无法在方法内部对节点对象进行具体的操作(如设置默认值、进行校验等),这些操作需要在调用方法之前或之后进行。