java組裝樹形結(jié)構(gòu)List問題
java組裝樹形結(jié)構(gòu)List
實(shí)現(xiàn)方式千千萬,下面是本人實(shí)現(xiàn)的一種方式。
業(yè)務(wù)需求
將一個內(nèi)關(guān)聯(lián)表中的數(shù)據(jù),組裝成樹形結(jié)構(gòu)。
業(yè)務(wù)表:使用parent_id關(guān)聯(lián)父節(jié)點(diǎn)id,父級根節(jié)點(diǎn)的parent_id為0。
樹形結(jié)構(gòu)
[ { "id": 1, "parentId": 0, "userName": "AA", "children": [ { "id": 11, "parentId": 1, "userName": "AA-AA", "children": [ { "id": 111, "parentId": 11, "userName": "AA-AA-AA", "children": [] } ] } ] }, { "id": 2, "parentId": 0, "userName": "BB", "children": [ { "id": 22, "parentId": 2, "userName": "BB-BB", "children": [] } ] } ]
java代碼
UserTree實(shí)體類:
import java.util.List; public class UserTree { private Integer id; private Integer parentId; private String userName; private List<UserTree> children; public UserTree(Integer id, Integer parentId, String userName) { this.id = id; this.parentId = parentId; this.userName = userName; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getParentId() { return parentId; } public void setParentId(Integer parentId) { this.parentId = parentId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public List<UserTree> getChildren() { return children; } public void setChildren(List<UserTree> children) { this.children = children; } @Override public String toString() { return "UserTree{" + "id=" + id + ", parentId=" + parentId + ", userName='" + userName + '\'' + ", children=" + children.toString() + '}'; } }
核心邏輯代碼
import java.util.ArrayList; import java.util.List; /** * 測試用戶樹形結(jié)構(gòu) * @author 測試 */ public class TestUserTree { public static void main(String[] args) { // 模擬從數(shù)據(jù)庫表中查詢出的 user數(shù)據(jù)list List<UserTree> userDataList = new ArrayList<>(); userDataList.add(new UserTree(1, 0, "AA")); userDataList.add(new UserTree(11, 1, "AA-AA")); userDataList.add(new UserTree(111, 11, "AA-AA-AA")); userDataList.add(new UserTree(2, 0, "BB")); userDataList.add(new UserTree(22, 2, "BB-BB")); // 父級根節(jié)點(diǎn) list List<UserTree> rootUserList = new ArrayList<>(); // 從查詢出的數(shù)據(jù)中 獲取 所有父級根節(jié)點(diǎn) for (UserTree user : userDataList) { // parentId為0的是父級 if (user.getParentId().equals(0)) { rootUserList.add(user); } } // 最終的樹形結(jié)構(gòu)list List<UserTree> userTreeList = new ArrayList<>(); // 構(gòu)建樹形結(jié)構(gòu) for (UserTree rootUser : rootUserList) { UserTree user = buildUserTree(userDataList, rootUser); userTreeList.add(user); } System.out.println(userTreeList); } /** * 遞歸構(gòu)建樹形結(jié)構(gòu) * @param userDataList 所有的用戶數(shù)據(jù)list * @param userTree 用戶對象 * @return */ public static UserTree buildUserTree(List<UserTree> userDataList, UserTree userTree) { List<UserTree> childrenUserList = new ArrayList<>(); for (UserTree user : userDataList) { // 當(dāng)前數(shù)據(jù)的 parentId 等于 父節(jié)點(diǎn)的 id,則該數(shù)據(jù)是當(dāng)前父級節(jié)點(diǎn)的子級。 if (user.getParentId().equals(userTree.getId())) { // 遞歸調(diào)用 childrenUserList.add(buildUserTree(userDataList, user)); } } userTree.setChildren(childrenUserList); return userTree; } }
最終打印出的就是上方提到的樹形結(jié)構(gòu)。
核心的思路就是,使用遞歸的方式逐級遍歷所有的用戶數(shù)據(jù),找出每一層父級節(jié)點(diǎn)的子級,將子級節(jié)點(diǎn)保存為list賦值給父級的children字段。
java組裝樹形結(jié)構(gòu)優(yōu)化思路
項(xiàng)目中經(jīng)常會遇到前端需要展現(xiàn)樹形結(jié)構(gòu)數(shù)據(jù),比如菜單樹、省市區(qū)聯(lián)動,在小數(shù)據(jù)量的時候,不管用什么算法都可以,但一旦數(shù)據(jù)大,不同算法的差距就會非常的大。
公司的項(xiàng)目中老代碼用的是遞歸方法構(gòu)建樹結(jié)構(gòu),2萬多個數(shù)據(jù)就需要跑20s,把生產(chǎn)服務(wù)器CPU都跑滿了,于是對該方法進(jìn)行重構(gòu)。
思路
- 為了不影響傳入的數(shù)據(jù),需要拷貝原數(shù)據(jù)集合。(增加和刪除頻繁,使用LinkedList結(jié)構(gòu))
- 拷貝的過程中,一并收集每個節(jié)點(diǎn)的id,后面用來判斷每個節(jié)點(diǎn)的parentId是否存在,不存在則認(rèn)為是根節(jié)點(diǎn)。(為了在比較的時候更快速,使用Set結(jié)構(gòu))
- 將集合中的數(shù)據(jù)用parentId作為key,放入map中,得到Map<String, List<TreeNode>>結(jié)構(gòu)數(shù)據(jù)。(利用Java8的Collectors.groupingBy獲?。?/li>
- 遍歷集合,將節(jié)點(diǎn)的id作為key,從第3步組裝的map中獲取子節(jié)點(diǎn),利用Java的地址引用構(gòu)建樹,并且將不是根節(jié)點(diǎn)的數(shù)據(jù)從集合移出。
實(shí)現(xiàn)代碼 (可以直接運(yùn)行)
TreeNode
@Data public class TreeNode { private String id; private String code; private String name; private String parentId; private List<TreeNode> children; }
實(shí)現(xiàn)代碼
public static List<TreeNode> buildTree(List<Map<String, Object>> oriDataMapList) { // 判空 if (oriDataMapList == null || oriDataMapList.isEmpty()) { return new ArrayList<>(); } // 最終要返回的只保留根節(jié)點(diǎn)的集合,增刪比較多,使用LinkedList List<TreeNode> respTreeNodeList = new LinkedList<>(); // id集合,后面用來判斷parentId是否在oriTreeNodeList存在,如果存在則說明是子節(jié)點(diǎn),不存在則認(rèn)為是根節(jié)點(diǎn) Set<String> idSet = new HashSet<>(oriDataMapList.size()); // 拷貝數(shù)據(jù) TreeNode treeNode; for (Map map : oriDataMapList) { treeNode = new TreeNode(); treeNode.setId((String) map.get("id")); treeNode.setCode((String) map.get("code")); treeNode.setName((String) map.get("name")); treeNode.setParentId((String) map.get("parentId")); // 遍歷的時候順便保存id集合 idSet.add(treeNode.getId()); respTreeNodeList.add(treeNode); } // 獲取由parentId作為key的map,同一個父節(jié)點(diǎn)的數(shù)據(jù)已經(jīng)被匯總成一個list,通過key就能獲取 Map<String, List<TreeNode>> parentIdMap = respTreeNodeList.stream().collect(Collectors.groupingBy(item -> item.getParentId() != null ? item.getParentId() : "nonParent")); // 設(shè)置children,并從根節(jié)點(diǎn)移出不是父節(jié)點(diǎn)的數(shù)據(jù) Iterator<TreeNode> iterator = respTreeNodeList.iterator(); TreeNode node; while (iterator.hasNext()) { node = iterator.next(); // 獲取當(dāng)前結(jié)點(diǎn)的子節(jié)點(diǎn)集合 node.setChildren(parentIdMap.get(node.getId())); // 如果父id在id集合中存在,則說明他不是根節(jié)點(diǎn),移除 if (idSet.contains(node.getParentId())) { iterator.remove(); } } return respTreeNodeList; }
測試代碼
public static void main(String[] args) { // 模擬測試數(shù)據(jù) List<Map<String, Object>> mapList = new ArrayList<>(); Map<String, Object> map; for (int i = 0; i < 1000; i++) { String parentId = String.valueOf(UUID.randomUUID()); map = new HashMap<>(); map.put("id", parentId); // %03d 長度為3,不夠的補(bǔ)0 map.put("code", String.format("%03d", i)); mapList.add(map); for (int j = 0; j < 1000; j++) { map = new HashMap<>(); String id = String.valueOf(UUID.randomUUID()); map.put("id", id); map.put("code", String.format("%03d", i) + String.format("%03d", j)); map.put("parentId", parentId); mapList.add(map); } } long start = System.currentTimeMillis(); List<TreeNode> treeNodeList = buildTree(mapList); String msg = String.format("組裝【%s】條數(shù)據(jù),耗時【%s】ms", mapList.size(), (System.currentTimeMillis() - start)); System.out.println(msg); }
運(yùn)行結(jié)果
組裝【1001000】條數(shù)據(jù),耗時【417】ms
結(jié)論:可以看到,跑一百萬的數(shù)據(jù)耗時只有不到0.5s,速度大大優(yōu)化
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的示例
本文主要介紹了MyBatis-Plus攔截器實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制的示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02spring和quartz整合,并簡單調(diào)用(實(shí)例講解)
下面小編就為大家?guī)硪黄猻pring和quartz整合,并簡單調(diào)用(實(shí)例講解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07Ubuntu安裝jenkins完成自動化構(gòu)建詳細(xì)步驟
Jenkins是一個開源的自動化服務(wù)器,可以用來輕松地建立持續(xù)集成和持續(xù)交付(CI/CD)管道,這篇文章主要給大家介紹了關(guān)于Ubuntu安裝jenkins完成自動化構(gòu)建的相關(guān)資料,需要的朋友可以參考下2024-03-03詳解如何使用Spring的@FeignClient注解實(shí)現(xiàn)通信功能
SpringBoot是一個非常流行的Java框架,它提供了一系列工具來使這種交互無縫且高效,在這些工具中,@FeignClient注解因其易用性和強(qiáng)大的功能而脫穎而出, 在這篇文章中,我們將探討如何使用Spring的@FeignClient注解進(jìn)行客戶端-服務(wù)器通信,需要的朋友可以參考下2023-11-11Spring Boot3.x自動配置不生效的排查與解決方法(IDEA 文件夾命名導(dǎo)致的問題)
在SpringBoot多模塊項(xiàng)目中,自動配置類未生效的問題通常源于文件路徑錯誤,通過檢查和修正AutoConfiguration.imports文件的實(shí)際路徑,可以解決自動配置不生效的問題,感興趣的朋友跟隨小編一起看看吧2024-11-11IntelliJ?IDEA?2023版本創(chuàng)建Spring項(xiàng)目時Java只能選擇17或21的問題解決方法
spring-boot是一個基于Java的開源框架,用于快速構(gòu)建生產(chǎn)級別的應(yīng)用程序,這篇文章主要給大家介紹了關(guān)于IntelliJ?IDEA?2023版本創(chuàng)建Spring項(xiàng)目時Java只能選擇17或21的問題解決方法,需要的朋友可以參考下2024-07-07IDEA中編寫并運(yùn)行shell腳本的實(shí)現(xiàn)
這篇文章主要介紹了IDEA中編寫并運(yùn)行shell腳本的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08