【Java工具類】List轉(zhuǎn)Tree通用工具類

一握爷、背景

最近的開發(fā)工作用到“樹”模型比較多,例如節(jié)點樹模叙、權(quán)限樹等歇拆。
每一個實體都有自己特殊的字段,所以最初的解決方法是給每一個實體寫?yīng)毺氐霓D(zhuǎn)換方法范咨。
然后需要轉(zhuǎn)換為樹的實體變多故觅,代碼就會冗余,因此寫一個工具類渠啊,提供對應(yīng)的方法將常見的List轉(zhuǎn)換為Tree输吏。

二、實現(xiàn)

1.效果

UI框架:layui(官方文檔:https://www.layui.com/doc/
在使用layui的過程中發(fā)現(xiàn)替蛉,目前l(fā)ayui的組件功能并沒有很強大贯溅,比如樹組件就不能實現(xiàn)“只選單個”拄氯,但是layui簡單易用,源碼也不復(fù)雜它浅,因此可以通過修改源碼來達到目的译柏。
后續(xù)計劃將自己在使用layui過程中遇到的問題和解決方法寫出來分享,歡迎各位碼友關(guān)注學(xué)習(xí)罚缕。

簡單的節(jié)點樹.png

2.編碼實現(xiàn)

2.1 實體TreeDot

前端框架實現(xiàn)樹組件艇纺,一般都有自己獨特的數(shù)據(jù)源格式。從layui文檔可看到layui實現(xiàn)樹組件的數(shù)據(jù)源格式:


layui樹形組件數(shù)據(jù)源格式.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;

/***
 * 樹點
 * 需要實現(xiàn)樹的類可以繼承該類,手寫set方法夜牡,在設(shè)定本身屬性值時同時設(shè)置該類中的相關(guān)屬性
 * @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;
    /**
     * 該點的圖標塘装,默認不設(shè)置
     */
    private String icon;
    /**
     * 該點的其他屬性
     */
    private Map<String,Object> attributes = new HashMap<>();
    /**
     * 該點的子樹集合
     */
    private List<TreeDot<T>> children = new ArrayList<>();

}

TreeDot類的字段和文檔提供的字段可能不完全一樣急迂,這個根據(jù)自己的實際需求來定就行。最為重要的是 id蹦肴、title僚碎、children 三個字段,layui根據(jù)這三個字段就可以構(gòu)建一顆樹阴幌,其他都可以不要勺阐,也可以新增自己額外需要的,例如 parentId矛双、attributes 渊抽,新增的字段不會影響樹的構(gòu)建。
另外议忽,這里用到了泛型懒闷,因為需要適應(yīng)所有需要轉(zhuǎn)換為樹的實體。

2.2 實體Node

建自己需要轉(zhuǎn)換成樹的原始實體栈幸,一般就是從數(shù)據(jù)源讀取的數(shù)據(jù)愤估。這個實體需要繼承上面建的TreeDot,并重寫set方法侦镇。這里以Node為例:

package pri.xiaowd.layui.pojo;

import lombok.Data;

/***
 * 節(jié)點
 * @ClassName Node
 * @Author xiaowd
 * @DateTime 2020/1/31 15:37
 */
@Data
public class Node extends TreeDot {

    /**
     * 節(jié)點編碼
     */
    private Integer nodeId;
    /**
     * 節(jié)點名稱
     */
    private String nodeName;
    /**
     * 父節(jié)點編碼
     */
    private Integer parentNodeId;
    /**
     * 創(chuàng)建時間
     */
    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,那么多余的字段就可以統(tǒng)一放在一個集合中attributes蒿赢。注意润樱,attributes需要一開始就實例化,不然會報錯哦羡棵。

2.3 工具類TreeDotUtils

接下來就是具體的工具類了壹若,這個工具類是通用的,因此需要使用泛型(同時使用了遞歸皂冰,就這兩個重要的知識點)店展,只要是繼承了TreeDot并重寫set方法的實體都可以轉(zhuǎn)化:

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轉(zhuǎn)換為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的點,說明是當(dāng)前點是頂級節(jié)點
                    TreeDot<T> tTreeDot = getTreeDotByT(t, tList);
                    treeDotList.add(tTreeDot);
                }
            }
        }
        return treeDotList;
    }

    /**
     * 根據(jù)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;
    }

    /**
     * 根據(jù)實體獲取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;
    }

    /**
     * 獲取根據(jù)指定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的所有點轉(zhuǎn)換為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(單點)的所有點轉(zhuǎn)換為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;
    }

}

在需要轉(zhuǎn)化時赂蕴,只需要調(diào)用 convertListToTreeDot(List<T> tList) 方法即可。參數(shù)List<T>是原始的實體集合舶胀。

  • 該方法在拿到原始的實體集合List<T>后概说,進行遍歷,通過將遍歷得到的實體的父ID傳入 isTreeDotExist(List<T> tList, String id) 方法判斷該實體的實體是否存在嚣伐,如果不存在說明就是根實體(PS.根實體也是一個集合糖赔,根不一定就只有一個);
  • 找到根實體之后轩端,通過 getTreeDotByT(T t,List<T> tList) 方法將該實體轉(zhuǎn)換為樹點TreeDot對象放典;該TreeDot對象中的children字段是子樹集合,通過 getChildTreeDotList(TreeDot<T> parentTreeDot, List<T> tList) 獲得船万;
  • getChildTreeDotList方法使用了遞歸刻撒。有兩個參數(shù),第一個是樹點TreeDot<T>耿导,要獲取哪個樹點的子樹集合声怔,這個參數(shù)就傳哪個樹點;第二個是原始的實體集合List<T>舱呻。
  • 遍歷原始的實體集合List<T>醋火,如果遍歷得到的實體的父ID是傳入的樹點的ID,那么這個實體就是傳入樹點的子樹集合其中一員箱吕,因此將該實體轉(zhuǎn)換為樹點并放到集合中芥驳,返回集合作為傳入樹點的子樹集合。
  • 因為這個轉(zhuǎn)換出來的樹點也有自己的子樹集合茬高,那么就需要通過遞歸獲取兆旬,只是第一個參數(shù)已經(jīng)變成了這個轉(zhuǎn)換出來的樹點。
  • 工具類中還額外提供了集合方法:
    getTreeDotById(String id,List<TreeDot<T>> treeDotList) : 根據(jù)ID獲取樹點怎栽;
    convertTreeDotToIdSet(List<TreeDot<T>> treeDotList,Class<K> kClass) : 根據(jù)樹集合(根有一到多個)獲取其ID的Set集合丽猬;
    convertTreeDotToIdSet(TreeDot<T> treeDot,Class<K> kClass) : 根據(jù)樹(根只有一個)獲取其ID的Set集合宿饱。

2.4 測試

測試的話就不從數(shù)據(jù)庫拿到List<T>了,手動建即可脚祟。

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

        //省節(jié)點 廣東
        Node pNode1 = new Node();
        pNode1.setNodeId(120000);
        pNode1.setNodeName("廣東");
        pNode1.setParentNodeId(110000);
        pNode1.setTime(System.currentTimeMillis());

        //市節(jié)點 廣州
        Node cNode1 = new Node();
        cNode1.setNodeId(120001);
        cNode1.setNodeName("廣州");
        cNode1.setParentNodeId(120000);
        cNode1.setTime(System.currentTimeMillis());

        //區(qū)節(jié)點 廣州
        Node aNode1 = new Node();
        aNode1.setNodeId(1200011);
        aNode1.setNodeName("天河區(qū)");
        aNode1.setParentNodeId(120001);
        aNode1.setTime(System.currentTimeMillis());

        //子節(jié)點 湖南
        Node pNode2 = new Node();
        pNode2.setNodeId(130000);
        pNode2.setNodeName("湖南");
        pNode2.setParentNodeId(110000);
        pNode2.setTime(System.currentTimeMillis());

        //市節(jié)點 長沙
        Node cNode2 = new Node();
        cNode2.setNodeId(130001);
        cNode2.setNodeName("長沙");
        cNode2.setParentNodeId(130000);
        cNode2.setTime(System.currentTimeMillis());

        //子節(jié)點 上海
        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);

        //轉(zhuǎn)換
        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: '無節(jié)點'
                }
            });
        });
        
        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

三谬以、結(jié)語

將List轉(zhuǎn)換為樹有不同的寫法,但是基本的思路是一樣的由桌,將這種轉(zhuǎn)換寫成一個工具類为黎,我認為對減少代碼冗余、優(yōu)化代碼美觀程度行您、提高編碼效率都有較大的幫助铭乾。如果各位碼友有自己的想法,或者覺得我的寫法還可以再優(yōu)化娃循,歡迎給我留言片橡,共同交流。
交流郵箱:weidag_xiao@sina.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淮野,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吹泡,更是在濱河造成了極大的恐慌骤星,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爆哑,死亡現(xiàn)場離奇詭異洞难,居然都是意外死亡,警方通過查閱死者的電腦和手機揭朝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門队贱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人潭袱,你說我怎么就攤上這事柱嫌。” “怎么了屯换?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵编丘,是天一觀的道長。 經(jīng)常有香客問我彤悔,道長嘉抓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任晕窑,我火速辦了婚禮抑片,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杨赤。我一直安慰自己敞斋,他們只是感情好截汪,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渺尘,像睡著了一般挫鸽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸥跟,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天丢郊,我揣著相機與錄音,去河邊找鬼医咨。 笑死枫匾,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拟淮。 我是一名探鬼主播干茉,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼很泊!你這毒婦竟也來了角虫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤委造,失蹤者是張志新(化名)和其女友劉穎戳鹅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昏兆,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡枫虏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了爬虱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隶债。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖跑筝,靈堂內(nèi)的尸體忽然破棺而出死讹,到底是詐尸還是另有隱情,我是刑警寧澤继蜡,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布回俐,位于F島的核電站,受9級特大地震影響稀并,放射性物質(zhì)發(fā)生泄漏仅颇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一碘举、第九天 我趴在偏房一處隱蔽的房頂上張望忘瓦。 院中可真熱鬧,春花似錦、人聲如沸耕皮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凌停。三九已至粱年,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罚拟,已是汗流浹背台诗。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赐俗,地道東北人拉队。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像阻逮,于是被迫代替她去往敵國和親粱快。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345