Java列表转树形结构的工具

不废话,直接上代码

一、工具函数

可以直接使用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
    }
]
相关推荐
公贵买其鹿16 分钟前
List深拷贝后,数据还是被串改
java
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭5 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫5 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp6 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea