【Java工具类】List转Tree通用工具类

一、背景

最近的开发工作用到“树”模型比较多,例如节点树、权限树等。
每一个实体都有自己特殊的字段,所以最初的解决方法是给每一个实体写独特的转换方法。
然后需要转换为树的实体变多,代码就会冗余,因此写一个工具类,提供对应的方法将常见的List转换为Tree。

二、实现

1.效果

UI框架:layui(官方文档:https://www.layui.com/doc/
在使用layui的过程中发现,目前layui的组件功能并没有很强大,比如树组件就不能实现“只选单个”,但是layui简单易用,源码也不复杂,因此可以通过修改源码来达到目的。
后续计划将自己在使用layui过程中遇到的问题和解决方法写出来分享,欢迎各位码友关注学习。

简单的节点树.png

2.编码实现

2.1 实体TreeDot

前端框架实现树组件,一般都有自己独特的数据源格式。从layui文档可看到layui实现树组件的数据源格式:


layui树形组件数据源格式.png

因此,首先建一个实体TreeDot,那么将这个实体返回前端就可以直接渲染成树。使用其他的UI框架类推。

package pri.xiaowd.layui.pojo;

import lombok.Data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/***
 * 树点
 * 需要实现树的类可以继承该类,手写set方法,在设定本身属性值时同时设置该类中的相关属性
 * @ClassName TreeTreeDot
 * @Author xiaowd
 * @DateTime 2020/2/23 15:58
 */
@Data
public class TreeDot<T> {

    /**
     * 点ID
     */
    private String id;
    /**
     * 点名称
     */
    private String title;
    /**
     * 父点ID
     */
    private String parentId;
    /**
     * 该点是否展开,默认不展开
     */
    private Boolean spread = false;
    /**
     * 该点是否选中,默认不选中
     */
    private Boolean checked = false;
    /**
     * 该点的图标,默认不设置
     */
    private String icon;
    /**
     * 该点的其他属性
     */
    private Map<String,Object> attributes = new HashMap<>();
    /**
     * 该点的子树集合
     */
    private List<TreeDot<T>> children = new ArrayList<>();

}

TreeDot类的字段和文档提供的字段可能不完全一样,这个根据自己的实际需求来定就行。最为重要的是 id、title、children 三个字段,layui根据这三个字段就可以构建一颗树,其他都可以不要,也可以新增自己额外需要的,例如 parentId、attributes ,新增的字段不会影响树的构建。
另外,这里用到了泛型,因为需要适应所有需要转换为树的实体。

2.2 实体Node

建自己需要转换成树的原始实体,一般就是从数据源读取的数据。这个实体需要继承上面建的TreeDot,并重写set方法。这里以Node为例:

package pri.xiaowd.layui.pojo;

import lombok.Data;

/***
 * 节点
 * @ClassName Node
 * @Author xiaowd
 * @DateTime 2020/1/31 15:37
 */
@Data
public class Node extends TreeDot {

    /**
     * 节点编码
     */
    private Integer nodeId;
    /**
     * 节点名称
     */
    private String nodeName;
    /**
     * 父节点编码
     */
    private Integer parentNodeId;
    /**
     * 创建时间
     */
    private Long time;

    public void setNodeId(Integer nodeId) {
        this.nodeId = nodeId;
        super.setId(String.valueOf(nodeId));
    }

    public void setNodeName(String nodeName) {
        this.nodeName = nodeName;
        super.setTitle(nodeName);
    }

    public void setParentNodeId(Integer parentNodeId) {
        this.parentNodeId = parentNodeId;
        super.setParentId(String.valueOf(parentNodeId));
    }

    public void setTime(Long time) {
        this.time = time;
        super.getAttributes().put("time",time);
    }

}

继承TreeDot之后,也就拥有了TreeDot的字段,因此重写set方法,将字段的值同时赋给TreeDot的字段。这里有一个小技巧,因为必要的字段是id和title,那么多余的字段就可以统一放在一个集合中attributes。注意,attributes需要一开始就实例化,不然会报错哦。

2.3 工具类TreeDotUtils

接下来就是具体的工具类了,这个工具类是通用的,因此需要使用泛型(同时使用了递归,就这两个重要的知识点),只要是继承了TreeDot并重写set方法的实体都可以转化:

package pri.xiaowd.layui.util;

import pri.xiaowd.layui.pojo.TreeDot;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/***
 * 操作“树”的工具
 * @ClassName TreeDotUtils
 * @Author xiaowd
 * @DateTime 2020/4/22 9:14
 */
public class TreeDotUtils {

    /**
     * 将List转换为Tree
     * @MethosName convertListToTreeDot
     * @Author xiaowd
     * @Date 2020/4/22 9:17
     * @param tList
     * @return java.util.List<cn.eshore.common.entity.Tree<T>>
     */
    public static <T extends TreeDot> List<TreeDot<T>> convertListToTreeDot(List<T> tList){
        List<TreeDot<T>> treeDotList = new ArrayList<>();
        if(tList != null && tList.size() > 0){
            for(T t:tList){
                if(!isTreeDotExist(tList,t.getParentId())){
                    //不存在以父ID为ID的点,说明是当前点是顶级节点
                    TreeDot<T> tTreeDot = getTreeDotByT(t, tList);
                    treeDotList.add(tTreeDot);
                }
            }
        }
        return treeDotList;
    }

    /**
     * 根据ID判断该点是否存在
     * @MethosName isTreeDotExist
     * @Author xiaowd
     * @Date 2020/4/22 9:50
     * @param tList
     * @param id 点ID
     * @return java.lang.Boolean
     */
    private static <T extends TreeDot> Boolean isTreeDotExist(List<T> tList, String id) {
        for(T t:tList){
            if(t.getId().equals(id)){
                return true;
            }
        }
        return false;
    }

    /**
     * 获取指定父点的子树
     * @MethosName getChildTreeList
     * @Author xiaowd
     * @Date 2020/4/22 10:02
     * @param parentTreeDot 父点
     * @param tList
     * @return java.util.List<cn.eshore.common.entity.Tree<T>>
     */
    private static <T extends TreeDot> List<TreeDot<T>> getChildTreeDotList(TreeDot<T> parentTreeDot, List<T> tList){
        List<TreeDot<T>> childTreeDotList = new ArrayList<>();
        for(T t:tList){
            if(parentTreeDot.getId().equals(t.getParentId())){
                //如果父ID是传递树点的ID,那么就是传递树点的子点
                TreeDot<T> tTreeDot = getTreeDotByT(t,tList);
                childTreeDotList.add(tTreeDot);
            }
        }
        return childTreeDotList;
    }

    /**
     * 根据实体获取TreeDot
     * @MethosName getTreeDotByT
     * @Author xiaowd
     * @Date 2020/5/4 22:17
     * @param t
     * @param tList
     * @return pri.xiaowd.layui.pojo.TreeDot<T>
     */
    private static <T extends TreeDot> TreeDot<T> getTreeDotByT(T t,List<T> tList){
        TreeDot<T> tTreeDot = new TreeDot<>();
        tTreeDot.setId(t.getId());
        tTreeDot.setParentId(t.getParentId());
        tTreeDot.setTitle(t.getTitle());
        tTreeDot.setChecked(t.getChecked());
        tTreeDot.setIcon(t.getIcon());
        tTreeDot.setAttributes(t.getAttributes());
        tTreeDot.setChildren(getChildTreeDotList(tTreeDot,tList));
        return tTreeDot;
    }

    /**
     * 获取根据指定ID所在点为父点的树
     * @MethosName getTreeDotById
     * @Author xiaowd
     * @Date 2020/4/22 15:00
     * @param id
     * @param treeDotList
     * @return cn.eshore.common.entity.TreeDot<T>
     */
    public static <T extends TreeDot> TreeDot<T> getTreeDotById(String id,List<TreeDot<T>> treeDotList){
        if(id != null && !"".equals(id) && treeDotList != null && treeDotList.size() > 0){
            for(TreeDot<T> treeDot:treeDotList){
                if(id.equalsIgnoreCase(treeDot.getId())){
                    return treeDot;
                }
                if(treeDot.getChildren() != null && treeDot.getChildren().size() > 0){
                    TreeDot<T> td = getTreeDotById(id, treeDot.getChildren());
                    if(td != null){
                        return td;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 将TreeList的所有点转换为ID的Set集合
     * @MethosName convertTreeDotToIdSet
     * @Author xiaowd
     * @Date 2020/4/22 16:13
     * @param treeDotList
     * @param kClass ID的类型
     * @return java.util.Set<K>
     */
    public static <T extends TreeDot,K> Set<K> convertTreeDotToIdSet(List<TreeDot<T>> treeDotList,Class<K> kClass){
        Set<K> idSet = new HashSet<>();
        if(treeDotList != null && treeDotList.size() > 0){
            for(TreeDot<T> treeDot:treeDotList){
                idSet.add((K)treeDot.getId());
                if(treeDot.getChildren() != null && treeDot.getChildren().size() > 0){
                    idSet.addAll(convertTreeDotToIdSet(treeDot.getChildren(),kClass));
                }
            }
        }
        return idSet;
    }

    /**
     * 将Tree(单点)的所有点转换为ID的Set集合
     * @MethosName convertTreeDotToIdSet
     * @Author xiaowd
     * @Date 2020/4/29 9:08
     * @param treeDot
     * @param kClass
     * @return java.util.Set<K>
     */
    public static <T extends TreeDot,K> Set<K> convertTreeDotToIdSet(TreeDot<T> treeDot,Class<K> kClass){
        Set<K> idSet = new HashSet<>();
        if(treeDot != null){
            idSet.add((K)treeDot.getId());
            if(treeDot.getChildren() != null && treeDot.getChildren().size() > 0){
                idSet.addAll(convertTreeDotToIdSet(treeDot.getChildren(),kClass));
            }
        }
        return idSet;
    }

}

在需要转化时,只需要调用 convertListToTreeDot(List<T> tList) 方法即可。参数List<T>是原始的实体集合。

  • 该方法在拿到原始的实体集合List<T>后,进行遍历,通过将遍历得到的实体的父ID传入 isTreeDotExist(List<T> tList, String id) 方法判断该实体的实体是否存在,如果不存在说明就是根实体(PS.根实体也是一个集合,根不一定就只有一个);
  • 找到根实体之后,通过 getTreeDotByT(T t,List<T> tList) 方法将该实体转换为树点TreeDot对象;该TreeDot对象中的children字段是子树集合,通过 getChildTreeDotList(TreeDot<T> parentTreeDot, List<T> tList) 获得;
  • getChildTreeDotList方法使用了递归。有两个参数,第一个是树点TreeDot<T>,要获取哪个树点的子树集合,这个参数就传哪个树点;第二个是原始的实体集合List<T>。
  • 遍历原始的实体集合List<T>,如果遍历得到的实体的父ID是传入的树点的ID,那么这个实体就是传入树点的子树集合其中一员,因此将该实体转换为树点并放到集合中,返回集合作为传入树点的子树集合。
  • 因为这个转换出来的树点也有自己的子树集合,那么就需要通过递归获取,只是第一个参数已经变成了这个转换出来的树点。
  • 工具类中还额外提供了集合方法:
    getTreeDotById(String id,List<TreeDot<T>> treeDotList) : 根据ID获取树点;
    convertTreeDotToIdSet(List<TreeDot<T>> treeDotList,Class<K> kClass) : 根据树集合(根有一到多个)获取其ID的Set集合;
    convertTreeDotToIdSet(TreeDot<T> treeDot,Class<K> kClass) : 根据树(根只有一个)获取其ID的Set集合。

2.4 测试

测试的话就不从数据库拿到List<T>了,手动建即可。

  • controller
    @RequestMapping("/tree")
    @ResponseBody
    public Map<String,Object> tree(){
        //国节点 中国
        Node rootNode = new Node();
        rootNode.setNodeId(110000);
        rootNode.setNodeName("中国");
        rootNode.setParentNodeId(0);
        rootNode.setTime(System.currentTimeMillis());

        //省节点 广东
        Node pNode1 = new Node();
        pNode1.setNodeId(120000);
        pNode1.setNodeName("广东");
        pNode1.setParentNodeId(110000);
        pNode1.setTime(System.currentTimeMillis());

        //市节点 广州
        Node cNode1 = new Node();
        cNode1.setNodeId(120001);
        cNode1.setNodeName("广州");
        cNode1.setParentNodeId(120000);
        cNode1.setTime(System.currentTimeMillis());

        //区节点 广州
        Node aNode1 = new Node();
        aNode1.setNodeId(1200011);
        aNode1.setNodeName("天河区");
        aNode1.setParentNodeId(120001);
        aNode1.setTime(System.currentTimeMillis());

        //子节点 湖南
        Node pNode2 = new Node();
        pNode2.setNodeId(130000);
        pNode2.setNodeName("湖南");
        pNode2.setParentNodeId(110000);
        pNode2.setTime(System.currentTimeMillis());

        //市节点 长沙
        Node cNode2 = new Node();
        cNode2.setNodeId(130001);
        cNode2.setNodeName("长沙");
        cNode2.setParentNodeId(130000);
        cNode2.setTime(System.currentTimeMillis());

        //子节点 上海
        Node pNode3 = new Node();
        pNode3.setNodeId(140000);
        pNode3.setNodeName("上海");
        pNode3.setParentNodeId(110000);
        pNode3.setTime(System.currentTimeMillis());

        List<Node> nodeList = new ArrayList<>();
        nodeList.add(rootNode);
        nodeList.add(pNode1);
        nodeList.add(cNode1);
        nodeList.add(aNode1);
        nodeList.add(pNode2);
        nodeList.add(cNode2);
        nodeList.add(pNode3);

        //转换
        List<TreeDot<Node>> nodeTreeDotList = TreeDotUtils.convertListToTreeDot(nodeList);

        Map<String,Object> result = new HashMap<>();
        result.put("nodeTreeDotList",nodeTreeDotList);

        return result;
    }
  • js
<script type="text/javascript" th:inline="javascript">
        var ctxPath=[[${#httpServletRequest.getContextPath()}]];
        layui.use(['tree'],function(){
            var tree = layui.tree;
            tree.render({
                elem: '#node-tree',
                id: 'node-tree',
                data: getNodeTreeDot(),
                onlyIconControl: true,
                edit: ['add'],
                customOperate: true,
                text: {
                    none: '无节点'
                }
            });
        });
        
        function getNodeTreeDot() {
            var data = [];
            layui.jquery.ajax({
                url: ctxPath + "/tree",
                type: "post",
                async: false,
                success: function(result){
                    data = result.nodeTreeDotList;
                }
            });
            console.log(data)
            return data;
        }
    </script>
  • 效果
最终展示效果.png

三、结语

将List转换为树有不同的写法,但是基本的思路是一样的,将这种转换写成一个工具类,我认为对减少代码冗余、优化代码美观程度、提高编码效率都有较大的帮助。如果各位码友有自己的想法,或者觉得我的写法还可以再优化,欢迎给我留言,共同交流。
交流邮箱:weidag_xiao@sina.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351