不废话,直接上代码
一、工具函数
可以直接使用list2tree()实现列表转树形结构
java
package com.server.utils.tree;
import org.springframework.beans.BeanUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author visy.wang
* @date 2024/6/27 21:27
*/
public class TreeUtil {
//通过Map的方式组装树形结构(只需单次遍历即可完成)
public static <T,K,R> R list2tree(List<T> list,
K rootId,
Function<T,K> idGetter,
Function<T,K> pidGetter,
Function<T,R> converter,
Supplier<R> builder,
BiConsumer<R,R> childAdder){
Map<K, R> map = new HashMap<>();
for (T t : list) {
K id = idGetter.apply(t), pid = pidGetter.apply(t);
//查找当前节点
R node = map.get(id);
if(node == null){//当前节点不存在则创建
node = converter.apply(t);
map.put(id, node);
}else{//当前节点已存在(被其他节点以父节点加入),补全剩余字段
R srcNode = converter.apply(t);
BeanUtils.copyProperties(srcNode, node, getNullProperties(srcNode));
}
//查找父节点
R parent = map.get(pid);
if(parent == null){//父节点不存在,则创建父节点,并将自身添加到父节点的子节点集合中
parent = builder.get();
childAdder.accept(parent, node);
map.put(pid, parent);
}else{//父节点已存在,直接将自身添加到父节点的子节点集合中
childAdder.accept(parent, node);
}
}
return map.get(rootId);
}
//通过递归的方式组装树形结构(层级过多时占用内存较大,数据不规范时有内存溢出风险)
public static <T,K,R> List<R> list2tree(List<T> list,
K rootId,
Function<T,K> idGetter,
Function<T,K> pidGetter,
Function<T,R> converter,
BiConsumer<R,List<R>> childrenSetter){
return list.stream().filter(t -> {
K parentId = pidGetter.apply(t);
return Objects.equals(parentId, rootId);
}).map(t -> {
K id = idGetter.apply(t);
R node = converter.apply(t);
List<R> children = list2tree(list, id, idGetter, pidGetter, converter, childrenSetter);
if(!children.isEmpty()){
childrenSetter.accept(node, children);
}
return node;
}).collect(Collectors.toList());
}
//通过Map+实现接口的方式组装树形结构
public static <T,K> List<TreeNode<K>> list2tree(List<T> list,
K rootId,
Function<T,TreeNode<K>> converter,
Supplier<TreeNode<K>> builder){
Map<K, TreeNode<K>> map = new HashMap<>();
for (T t : list) {
TreeNode<K> node = converter.apply(t);
K id = node.getId(), parentId = node.getParentId();
//查找当前节点
TreeNode<K> currNode = map.get(id);
if(currNode != null){//当前节点已存在(被其他节点以父节点加入)
//复制子节点集合
node.setChildren(currNode.getChildren());
}
map.put(id, node);//更新或添加当前节点
//查找父节点
TreeNode<K> parentNode = map.get(parentId);
if(parentNode == null){//父节点不存在,则创建父节点,并将自身添加到父节点的子节点集合中
parentNode = builder.get();
parentNode.addChild(node);
map.put(parentId, parentNode);
}else{//父节点已存在,直接将自身添加到父节点的子节点集合中
parentNode.addChild(node);
}
}
TreeNode<K> rootNode = map.get(rootId);
return rootNode==null ? Collections.emptyList() : rootNode.getChildren();
}
//通过递归+实现接口的方式组装树形结构
public static <T,K> List<TreeNode<K>> list2tree(List<T> list,
K rootId,
Function<T,TreeNode<K>> converter){
return list.stream().map(converter).filter(node -> {
K parentId = node.getParentId();
return Objects.equals(parentId, rootId);
}).peek(node -> {
K id = node.getId();
List<TreeNode<K>> children = list2tree(list, id, converter);
if(!children.isEmpty()){
node.setChildren(children);
}
}).collect(Collectors.toList());
}
private static final Map<String,Field[]> fieldsCache = new HashMap<>();
private static String[] getNullProperties(Object obj) {
Class<?> clazz = obj.getClass();
String className = clazz.getName();
Field[] fields = fieldsCache.get(className);
if(fields == null){
fields = clazz.getDeclaredFields();
Field.setAccessible(fields, true);
fieldsCache.put(className, fields);
}
List<String> nullProperties = new ArrayList<>();
for (Field field : fields) {
try {
Object value = field.get(obj);
if (value == null) {
nullProperties.add(field.getName());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
String[] result = new String[nullProperties.size()];
return nullProperties.toArray(result);
}
}
二、接口定义
定义节点规范
java
package com.server.utils.tree;
import java.util.List;
/**
* @author visy.wang
* @date 2024/7/1 10:45
*/
public interface TreeNode<K> {
K getId();
K getParentId();
void addChild(TreeNode<K> child);
List<TreeNode<K>> getChildren();
void setChildren(List<TreeNode<K>> children);
}
三、原始对象
java
package com.server.utils.tree;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 菜单
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单id
*/
private Long id;
/**
* 父id
*/
private Long fid;
/**
* 机构名称
*/
private String name;
/**
* 模块id
*/
private Integer level;
/**
* 状态 1 启用 2 停用
*/
private Integer status;
/**
* 权重
*/
private Integer weight;
}
四、节点对象
java
package com.server.utils.tree;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
/**
* @author visy.wang
* @date 2024/6/27 21:54
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MenuNode extends Menu { //不一定要继承原始对象(字段都能复用的时候才考虑继承)
/**
* 是否勾选
*/
private Integer isCheck;
/**
* 子菜单列表
*/
private List<MenuNode> children;
public void addChild(MenuNode child){
if(children == null){
children = new ArrayList<>();
}
children.add(child);
}
}
五、测试
java
package com.server.utils.tree;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author visy.wang
* @date 2024/6/27 21:55
*/
public class Test {
public static void main(String[] args) {
List<Menu> menuList = new ArrayList<>();
//顺序可以任意调整,不影响结果
menuList.add(new Menu(1L, null, "菜单A", 1, 1,1));
menuList.add(new Menu(4L, 2L, "菜单BA", 2, 1,4));
menuList.add(new Menu(3L, 1L, "菜单AA", 2, 1,3));
menuList.add(new Menu(5L, 3L, "菜单AAA", 3, 1,5));
menuList.add(new Menu(2L, null, "菜单B", 1, 1,2));
//勾选的菜单ID集合
Set<Long> checkedMenuIds = new HashSet<>();
checkedMenuIds.add(3L);
checkedMenuIds.add(5L);
//map的方式
MenuNode root = TreeUtil.list2tree(
menuList, //原始列表
null, //根节点ID,用于提取顶层节点
Menu::getId, //获取ID的方法,也可以指定别的字段
Menu::getFid, //获取父ID的方法,也可以指定别的字段,但是必须和上面的方法对应
menu -> { //将列表中的原始对象转换成节点对象(一般来说比原始对象多了对子节点集合的持有,除此之外也可以按需要增减字段)
MenuNode node = new MenuNode();//创建一个节点
BeanUtils.copyProperties(menu, node);//复制原始对象的字段到节点对象
node.setIsCheck(checkedMenuIds.contains(menu.getId()) ? 1 : 0);//单独设置其他字段
return node;//返回节点对象
},
MenuNode::new, //节点对象的构造方法,用于创建一个新的父节点对象
MenuNode::addChild //添加子节点的方法
);
System.out.println(JSON.toJSONString(root.getChildren()));
//递归的方式
List<MenuNode> menuNodeList = TreeUtil.list2tree(
menuList, //原始列表
null, //根节点ID
Menu::getId, //获取ID的方法,也可以指定别的字段
Menu::getFid, //获取父ID的方法,也可以指定别的字段,但是必须和上面的方法对应
menu -> { //将列表中的原始对象转换成节点对象(一般来说比原始对象多了对子节点集合的持有,除此之外也可以按需要增减字段)
MenuNode node = new MenuNode();//创建一个节点
BeanUtils.copyProperties(menu, node);//复制原始对象的字段到节点对象
node.setIsCheck(checkedMenuIds.contains(menu.getId()) ? 1 : 0);//单独设置其他字段
return node;//返回节点对象
},
MenuNode::setChildren //设置子节点集合的方法
);
System.out.println(JSON.toJSONString(menuNodeList));
}
}
六、打印结果
- map的方式:
json
[
{
"children": [
{
"children": [
{
"fid": 3,
"id": 5,
"isCheck": 1,
"level": 3,
"name": "菜单AAA",
"status": 1,
"weight": 5
}
],
"fid": 1,
"id": 3,
"isCheck": 1,
"level": 2,
"name": "菜单AA",
"status": 1,
"weight": 3
}
],
"id": 1,
"isCheck": 0,
"level": 1,
"name": "菜单A",
"status": 1,
"weight": 1
},
{
"children": [
{
"fid": 2,
"id": 4,
"isCheck": 0,
"level": 2,
"name": "菜单BA",
"status": 1,
"weight": 4
}
],
"id": 2,
"isCheck": 0,
"level": 1,
"name": "菜单B",
"status": 1,
"weight": 2
}
]
- 递归的方式:
json
[
{
"children": [
{
"children": [
{
"fid": 3,
"id": 5,
"isCheck": 1,
"level": 3,
"name": "菜单AAA",
"status": 1,
"weight": 5
}
],
"fid": 1,
"id": 3,
"isCheck": 1,
"level": 2,
"name": "菜单AA",
"status": 1,
"weight": 3
}
],
"id": 1,
"isCheck": 0,
"level": 1,
"name": "菜单A",
"status": 1,
"weight": 1
},
{
"children": [
{
"fid": 2,
"id": 4,
"isCheck": 0,
"level": 2,
"name": "菜单BA",
"status": 1,
"weight": 4
}
],
"id": 2,
"isCheck": 0,
"level": 1,
"name": "菜单B",
"status": 1,
"weight": 2
}
]
七、实现接口的方式
- 节点对象
节点对象必须实现TreeNode接口,泛型中指定子父关联字段的类型
java
package com.server.utils.tree;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.List;
/**
* @author visy.wang
* @date 2024/7/1 11:31
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MenuNodeV2 extends Menu implements TreeNode<Long>{
/**
* 是否勾选
*/
private Integer isCheck;
/**
* 子菜单列表
*/
private List<TreeNode<Long>> children;
@Override
public Long getParentId() {
return getFid();
}
@Override
public void addChild(TreeNode<Long> child) {
if(this.children == null){
this.children = new ArrayList<>();
}
this.children.add(child);
}
}
- 测试
java
package com.server.utils.tree;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author visy.wang
* @date 2024/6/27 21:55
*/
public class Test {
public static void main(String[] args) {
List<Menu> menuList = new ArrayList<>();
//顺序可以任意调整,不影响结果
menuList.add(new Menu(1L, null, "菜单A", 1, 1,1));
menuList.add(new Menu(4L, 2L, "菜单BA", 2, 1,4));
menuList.add(new Menu(3L, 1L, "菜单AA", 2, 1,3));
menuList.add(new Menu(5L, 3L, "菜单AAA", 3, 1,5));
menuList.add(new Menu(2L, null, "菜单B", 1, 1,2));
//勾选的菜单ID集合
Set<Long> checkedMenuIds = new HashSet<>();
checkedMenuIds.add(3L);
checkedMenuIds.add(5L);
//map的方式+接口实现
List<TreeNode<Long>> treeNodeList = TreeUtil.list2tree(
menuList, //原始列表
null, //根节点ID,用于提取顶层节点
menu -> { //将列表中的原始对象
MenuNodeV2 node = new MenuNodeV2();//创建一个节点
BeanUtils.copyProperties(menu, node);//复制原始对象的字段到节点对象
node.setIsCheck(checkedMenuIds.contains(menu.getId()) ? 1 : 0);//单独设置其他字段
return node;//返回节点对象
},
MenuNodeV2::new
);
System.out.println(JSON.toJSONString(treeNodeList));
//递归的方式+接口实现
List<TreeNode<Long>> treeNodeList2 = TreeUtil.list2tree(
menuList, //原始列表
null, //根节点ID,用于提取顶层节点
menu -> { //将列表中的原始对象
MenuNodeV2 node = new MenuNodeV2();//创建一个节点
BeanUtils.copyProperties(menu, node);//复制原始对象的字段到节点对象
node.setIsCheck(checkedMenuIds.contains(menu.getId()) ? 1 : 0);//单独设置其他字段
return node;//返回节点对象
}
);
System.out.println(JSON.toJSONString(treeNodeList2));
}
}
- 打印结果
map的方式+接口实现
json
[
{
"children": [
{
"children": [
{
"fid": 3,
"id": 5,
"isCheck": 1,
"level": 3,
"name": "菜单AAA",
"parentId": 3,
"status": 1,
"weight": 5
}
],
"fid": 1,
"id": 3,
"isCheck": 1,
"level": 2,
"name": "菜单AA",
"parentId": 1,
"status": 1,
"weight": 3
}
],
"id": 1,
"isCheck": 0,
"level": 1,
"name": "菜单A",
"status": 1,
"weight": 1
},
{
"children": [
{
"fid": 2,
"id": 4,
"isCheck": 0,
"level": 2,
"name": "菜单BA",
"parentId": 2,
"status": 1,
"weight": 4
}
],
"id": 2,
"isCheck": 0,
"level": 1,
"name": "菜单B",
"status": 1,
"weight": 2
}
]
递归的方式+接口实现
json
[
{
"children": [
{
"children": [
{
"fid": 3,
"id": 5,
"isCheck": 1,
"level": 3,
"name": "菜单AAA",
"parentId": 3,
"status": 1,
"weight": 5
}
],
"fid": 1,
"id": 3,
"isCheck": 1,
"level": 2,
"name": "菜单AA",
"parentId": 1,
"status": 1,
"weight": 3
}
],
"id": 1,
"isCheck": 0,
"level": 1,
"name": "菜单A",
"status": 1,
"weight": 1
},
{
"children": [
{
"fid": 2,
"id": 4,
"isCheck": 0,
"level": 2,
"name": "菜单BA",
"parentId": 2,
"status": 1,
"weight": 4
}
],
"id": 2,
"isCheck": 0,
"level": 1,
"name": "菜单B",
"status": 1,
"weight": 2
}
]