Java使用 Stream 流和 Lambda 組裝復(fù)雜父子樹(shù)形結(jié)構(gòu)
前言
在最近的開(kāi)發(fā)中,遇到了兩個(gè)類(lèi)似的需求:都是基于 Stream 的父子樹(shù)形結(jié)構(gòu)操作,返回 List 集合對(duì)象給前端。于是在經(jīng)過(guò)需求分析和探索實(shí)踐后有了新的認(rèn)識(shí),現(xiàn)在拿出來(lái)和大家作分享交流。
一般來(lái)說(shuō)完成這樣的需求大多數(shù)人會(huì)想到遞歸,但遞歸的方式弊端過(guò)于明顯:方法多次自調(diào)用效率很低、數(shù)據(jù)量大容易導(dǎo)致堆棧溢出、隨著樹(shù)深度的增加其時(shí)間復(fù)雜度會(huì)呈指數(shù)級(jí)增加等。
核心思路如下:
數(shù)據(jù)庫(kù)全量查詢(xún)(幾萬(wàn)條),內(nèi)存中使用 stream 流操作、Lambda 表達(dá)式、Java 地址引用
使用緩存注解(底層Redis分布式緩存實(shí)現(xiàn)),過(guò)期后自動(dòng)更新緩存,再次調(diào)用接口則先命中緩存,沒(méi)有的話(huà)再查數(shù)據(jù)庫(kù)
使用 MQ 來(lái)做異步通知更新,即當(dāng)數(shù)據(jù)有更改時(shí),可以異步將數(shù)據(jù)先更新,再寫(xiě)入緩存,使業(yè)務(wù)更合理,考慮更全面
一、以企業(yè)部門(mén)結(jié)構(gòu)為例
這里的實(shí)體是放在 MySQL 里的,使用簡(jiǎn)單的封裝好的查詢(xún)語(yǔ)句,這個(gè)很簡(jiǎn)單。
1.1實(shí)體
部門(mén)表:一個(gè)公司里都會(huì)有許多的部門(mén),一個(gè)部門(mén)里還會(huì)有部門(mén)。從最頂層到你所在的的部門(mén),可能會(huì)有多達(dá)六、七層。以下只展示核心字段:
@Data
public class Department {
/**
* 主鍵Id
*/
private Integer id;
/**
* 該部門(mén)的父部門(mén)Id
*/
private Integer parentDeptId;
/**
* 真正部門(mén)Id
*/
private Integer deptId;
/**
* 部門(mén)的名稱(chēng)
*/
private String name;
/**
* 部門(mén)在結(jié)構(gòu)中所處的層級(jí)
*/
private Integer level;
/**
* 狀態(tài)是否啟用
*/
private Integer status;
}
1.2返回VO
這個(gè)返回的VO是給前端的,里面的子節(jié)點(diǎn)集合屬性 childrenList,是一個(gè)關(guān)鍵字段,所有該方式返回樹(shù)結(jié)構(gòu)的 VO 都需要有該字段來(lái)”封裝自己“。
@Data
public class DepartmentVO implements Serializable {
/**
* 子節(jié)點(diǎn)集合,封裝自己
*/
private List<DepartmentVO> childrenList;
/**
* 部門(mén)Id
*/
protected Integer deptId;
/**
* 父部門(mén)Id
*/
protected Integer parentDeptId;
/**
* 部門(mén)名稱(chēng)
*/
protected String name;
}
1.3具體實(shí)現(xiàn)
下面直接上 demo 代碼,注釋已經(jīng)說(shuō)的比較清楚了:
@Override
public List<DepartmentVO> departmentStructure(String id){
//step1:這里 map 只是簡(jiǎn)單轉(zhuǎn)換了返回的對(duì)象屬性(返回需要的類(lèi)型),本質(zhì)還是所有部門(mén)數(shù)據(jù)
List<DepartmentVO> departmentVOList = this.getDepartmentListById(id).stream()
.map(e -> e.copyProperties(DepartmentVO.class))
.collect(Collectors.toList());
//step2:利用父節(jié)點(diǎn)分組,所有部門(mén)的父 Id 進(jìn)行分組,把所有的子節(jié)點(diǎn) List 集合都找出來(lái)并一層層分好組
Map<Integer, List<DepartmentVO>> departmentListMap = departmentVOList.stream()
.collect(Collectors.groupingBy(DepartmentVO::getParentDeptId));
//step3:關(guān)鍵一步,關(guān)聯(lián)上子部門(mén),將子部門(mén)的 List 集合經(jīng)過(guò)遍歷一層層地放置好,最終會(huì)得到完整的部門(mén)父子關(guān)系 List 集合
departmentVOList.forEach(e -> e.setChildrenList(departmentListMap.get(e.getDeptId())));
//step4:過(guò)濾出頂級(jí)部門(mén),即所有的子部門(mén)數(shù)據(jù)都?xì)w屬于一個(gè)頂級(jí)父部門(mén) Id
List<DepartmentVO> resultList = departmentVOList.stream()
.filter(e -> Constants.TOP_DEPARTMENT_NUM.equals(e.getParentDeptId()))
.collect(Collectors.toList());
return Optional.of(resultList).orElse(null);
}
1.4效果展示
我這里測(cè)試的例子是只有三層,數(shù)據(jù)也沒(méi)有完全展開(kāi),當(dāng)然五六層也是沒(méi)問(wèn)題的。
只要總的部門(mén)數(shù)據(jù)量在一兩萬(wàn)條以?xún)?nèi)(啥情況部門(mén)數(shù)量會(huì)有幾萬(wàn)個(gè)?部門(mén)表一般是獨(dú)立于其它表的)速度都是比較快的,服務(wù)器性能(主要內(nèi)存給力)好的話(huà),基本整個(gè)請(qǐng)求/響應(yīng)(拋開(kāi)網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。

部門(mén)結(jié)構(gòu)效果圖
二、以中國(guó)行政區(qū)域結(jié)構(gòu)為例
實(shí)體只需要使用一次查全量的語(yǔ)句,沒(méi)有其它別的操作,很大程度上是因?yàn)槭∈锌h的結(jié)構(gòu)是比較固定的。
2.1實(shí)體
全國(guó)行政區(qū)表:全國(guó)的行政區(qū)包括省/直轄市/自治區(qū)、地級(jí)市、區(qū)/縣級(jí)市/縣這三級(jí),再往下的街道/鎮(zhèn)、以及下面的村/小組就不包含了。同樣也是只留關(guān)鍵屬性:
@Data
public class Area {
/**
* 地區(qū)id
*/
public Long id;
/**
* 父Id
*/
public Long parentId;
/**
* 地區(qū)名稱(chēng)
*/
public String name;
/**
* 所屬省Id
*/
public Long provinceId;
/**
* 所屬地級(jí)市Id
*/
public Long cityId;
/**
* 所處層級(jí)
*/
public Integer level;
}
2.2返回VO
同樣,這個(gè)里面的子節(jié)點(diǎn)集合屬性 childrenAreaVOList,是一個(gè)關(guān)鍵字段,所有該方式返回樹(shù)結(jié)構(gòu)的 VO 都需要有該字段來(lái)”封裝自己“。
@Data
public class AreaVO {
/**
* 子節(jié)點(diǎn) list 集合
*/
private List<AreaVO> childrenAreaVOList;
/**
* 區(qū)域id
*/
public Long id;
/**
* 地區(qū)名稱(chēng)
*/
public String name;
/**
* 所處層級(jí)
*/
public Integer level;
/**
* 父Id
*/
public Long parentId;
/**
* 所屬省Id
*/
public Long provinceId;
/**
* 所屬地級(jí)市Id
*/
public Long cityId;
}
2.3具體實(shí)現(xiàn)
下面同樣直接上 demo 代碼,注釋比較詳細(xì):
@Override
public List<AreaVO> getAreaStructure() {
//第一步,從數(shù)據(jù)庫(kù)中查出所有數(shù)據(jù),按照排序條件進(jìn)行排序,本質(zhì)上還是這個(gè)所有數(shù)據(jù)的 List 集合
List<AreaVO> areaVOList = this.findAll(Sort.by("id").descending()).stream()
//注:這里使用 map 映射了需要返回的 VO,即相同的屬性字段就會(huì)轉(zhuǎn)換
.map(e -> e.copyProperties(AreaVO.class)).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(areaVOList)){
//第二步,根據(jù)父Id 字段進(jìn)行分組,即所有數(shù)據(jù)都會(huì)按照第一層至最后一層都按照父子關(guān)系進(jìn)行分組;注意,是對(duì)所有數(shù)據(jù)分組
Map<Long, List<AreaVO>> areaVoListMap = areaVOList.parallelStream().collect(Collectors.groupingBy(AreaVO::getParentId));
//第三步,也是最關(guān)鍵的一步,將所有子數(shù)據(jù) List 集合經(jīng)過(guò)遍歷后都一層層地放置好,最終會(huì)得到一個(gè)包含父子關(guān)系的完整List
areaVOList.forEach(e -> e.setChildrenAreaVOList(areaVoListMap.get(e.getId())));
//第四步,過(guò)濾出符合頂層父Id的所有數(shù)據(jù),即所有數(shù)據(jù)都?xì)w屬于一個(gè)頂層父Id
List<AreaVO> resultList = areaVOList.stream()
.filter(e -> Constants.COUNTRY_CHINA_TOP_NUM.equals(e.getParentId()))
.collect(Collectors.toList());
return Optional.of(resultList).orElse(null);
}
return new ArrayList<>();
}
2.4效果展示
我這里測(cè)試環(huán)境的例子是只有省/直轄市/自治區(qū)、地級(jí)市、區(qū)/縣級(jí)市/縣這三級(jí),數(shù)據(jù)也沒(méi)有完全展開(kāi),當(dāng)然到下面的鎮(zhèn)/街道,乃至村/小組也是沒(méi)問(wèn)題的。
這里總的測(cè)試數(shù)據(jù)量是幾千條,如果加上鎮(zhèn)/街道應(yīng)該得有幾萬(wàn)條,速度也還是是比較快的,服務(wù)器性能(主要內(nèi)存給力)好的話(huà),基本整個(gè)請(qǐng)求/響應(yīng)(拋開(kāi)網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。

中國(guó)行政區(qū)域信息層次結(jié)構(gòu)效果
時(shí)間消耗,這里響應(yīng)只有兩百多毫秒,如下圖的接口的性能展示:

接口性能展示
三、文章小結(jié)
使用 Stream 流組裝復(fù)雜父子樹(shù)形結(jié)構(gòu)(List 集合形式)的分享到這里就結(jié)束了,編碼沒(méi)有捷徑,都是項(xiàng)目實(shí)踐里出真知,一點(diǎn)點(diǎn)摸索攢經(jīng)驗(yàn)。
- Java如何使用遞歸查詢(xún)多級(jí)樹(shù)形結(jié)構(gòu)數(shù)據(jù)(多級(jí)菜單)
- java+vue3+el-tree實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)操作代碼
- java返回前端樹(shù)形結(jié)構(gòu)數(shù)據(jù)的2種實(shí)現(xiàn)方式
- 詳解如何使用Java流API構(gòu)建樹(shù)形結(jié)構(gòu)數(shù)據(jù)
- java如何讀取文件目錄返回樹(shù)形結(jié)構(gòu)
- Java中如何將list轉(zhuǎn)為樹(shù)形結(jié)構(gòu)
- Java樹(shù)形結(jié)構(gòu)遞歸查詢(xún)方式
相關(guān)文章
又又叕出BUG啦!理智分析Java NIO的ByteBuffer到底有多難用
網(wǎng)絡(luò)數(shù)據(jù)的基本單位永遠(yuǎn)是byte,Java NIO提供ByteBuffer作為字節(jié)的容器,但該類(lèi)過(guò)于復(fù)雜,有點(diǎn)難用.本篇文章就帶大家簡(jiǎn)單了解一下 ,需要的朋友可以參考下2021-06-06
Java虛擬機(jī)常見(jiàn)內(nèi)存溢出錯(cuò)誤匯總
這篇文章主要匯總了Java虛擬機(jī)常見(jiàn)的內(nèi)存溢出錯(cuò)誤,警示大家,避免出錯(cuò),感興趣的朋友可以了解下2020-09-09
jenkins+maven+svn自動(dòng)部署和發(fā)布的詳細(xì)圖文教程
Jenkins是一個(gè)開(kāi)源的、可擴(kuò)展的持續(xù)集成、交付、部署的基于web界面的平臺(tái)。這篇文章主要介紹了jenkins+maven+svn自動(dòng)部署和發(fā)布的詳細(xì)圖文教程,需要的朋友可以參考下2020-09-09
idea構(gòu)建web項(xiàng)目的超級(jí)詳細(xì)教程
好多朋友在使用IDEA創(chuàng)建項(xiàng)目時(shí),總會(huì)碰到一些小問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于idea構(gòu)建web項(xiàng)目的超級(jí)詳細(xì)教程,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03
JAVA提高第八篇 動(dòng)態(tài)代理技術(shù)
這篇文章主要為大家詳細(xì)介紹了JAVA動(dòng)態(tài)代理技術(shù)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
排查Failed?to?validate?connection?com.mysql.cj.jdbc.Connec
這篇文章主要介紹了Failed?to?validate?connection?com.mysql.cj.jdbc.ConnectionImpl問(wèn)題排查,具有很好的參考價(jià)值,希望對(duì)大家有所幫助2023-02-02
Java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)(使用數(shù)據(jù)庫(kù))
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),使用數(shù)據(jù)庫(kù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01

