Java使用 Stream 流和 Lambda 組裝復雜父子樹形結(jié)構(gòu)
前言
在最近的開發(fā)中,遇到了兩個類似的需求:都是基于 Stream 的父子樹形結(jié)構(gòu)操作,返回 List 集合對象給前端。于是在經(jīng)過需求分析和探索實踐后有了新的認識,現(xiàn)在拿出來和大家作分享交流。
一般來說完成這樣的需求大多數(shù)人會想到遞歸,但遞歸的方式弊端過于明顯:方法多次自調(diào)用效率很低、數(shù)據(jù)量大容易導致堆棧溢出、隨著樹深度的增加其時間復雜度會呈指數(shù)級增加等。
核心思路如下:
數(shù)據(jù)庫全量查詢(幾萬條),內(nèi)存中使用 stream 流操作、Lambda 表達式、Java 地址引用
使用緩存注解(底層Redis分布式緩存實現(xiàn)),過期后自動更新緩存,再次調(diào)用接口則先命中緩存,沒有的話再查數(shù)據(jù)庫
使用 MQ 來做異步通知更新,即當數(shù)據(jù)有更改時,可以異步將數(shù)據(jù)先更新,再寫入緩存,使業(yè)務更合理,考慮更全面
一、以企業(yè)部門結(jié)構(gòu)為例
這里的實體是放在 MySQL 里的,使用簡單的封裝好的查詢語句,這個很簡單。
1.1實體
部門表:一個公司里都會有許多的部門,一個部門里還會有部門。從最頂層到你所在的的部門,可能會有多達六、七層。以下只展示核心字段:
@Data public class Department { /** * 主鍵Id */ private Integer id; /** * 該部門的父部門Id */ private Integer parentDeptId; /** * 真正部門Id */ private Integer deptId; /** * 部門的名稱 */ private String name; /** * 部門在結(jié)構(gòu)中所處的層級 */ private Integer level; /** * 狀態(tài)是否啟用 */ private Integer status; }
1.2返回VO
這個返回的VO是給前端的,里面的子節(jié)點集合屬性 childrenList,是一個關(guān)鍵字段,所有該方式返回樹結(jié)構(gòu)的 VO 都需要有該字段來”封裝自己“。
@Data public class DepartmentVO implements Serializable { /** * 子節(jié)點集合,封裝自己 */ private List<DepartmentVO> childrenList; /** * 部門Id */ protected Integer deptId; /** * 父部門Id */ protected Integer parentDeptId; /** * 部門名稱 */ protected String name; }
1.3具體實現(xiàn)
下面直接上 demo 代碼,注釋已經(jīng)說的比較清楚了:
@Override public List<DepartmentVO> departmentStructure(String id){ //step1:這里 map 只是簡單轉(zhuǎn)換了返回的對象屬性(返回需要的類型),本質(zhì)還是所有部門數(shù)據(jù) List<DepartmentVO> departmentVOList = this.getDepartmentListById(id).stream() .map(e -> e.copyProperties(DepartmentVO.class)) .collect(Collectors.toList()); //step2:利用父節(jié)點分組,所有部門的父 Id 進行分組,把所有的子節(jié)點 List 集合都找出來并一層層分好組 Map<Integer, List<DepartmentVO>> departmentListMap = departmentVOList.stream() .collect(Collectors.groupingBy(DepartmentVO::getParentDeptId)); //step3:關(guān)鍵一步,關(guān)聯(lián)上子部門,將子部門的 List 集合經(jīng)過遍歷一層層地放置好,最終會得到完整的部門父子關(guān)系 List 集合 departmentVOList.forEach(e -> e.setChildrenList(departmentListMap.get(e.getDeptId()))); //step4:過濾出頂級部門,即所有的子部門數(shù)據(jù)都歸屬于一個頂級父部門 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效果展示
我這里測試的例子是只有三層,數(shù)據(jù)也沒有完全展開,當然五六層也是沒問題的。
只要總的部門數(shù)據(jù)量在一兩萬條以內(nèi)(啥情況部門數(shù)量會有幾萬個?部門表一般是獨立于其它表的)速度都是比較快的,服務器性能(主要內(nèi)存給力)好的話,基本整個請求/響應(拋開網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。
部門結(jié)構(gòu)效果圖
二、以中國行政區(qū)域結(jié)構(gòu)為例
實體只需要使用一次查全量的語句,沒有其它別的操作,很大程度上是因為省市縣的結(jié)構(gòu)是比較固定的。
2.1實體
全國行政區(qū)表:全國的行政區(qū)包括省/直轄市/自治區(qū)、地級市、區(qū)/縣級市/縣這三級,再往下的街道/鎮(zhèn)、以及下面的村/小組就不包含了。同樣也是只留關(guān)鍵屬性:
@Data public class Area { /** * 地區(qū)id */ public Long id; /** * 父Id */ public Long parentId; /** * 地區(qū)名稱 */ public String name; /** * 所屬省Id */ public Long provinceId; /** * 所屬地級市Id */ public Long cityId; /** * 所處層級 */ public Integer level; }
2.2返回VO
同樣,這個里面的子節(jié)點集合屬性 childrenAreaVOList,是一個關(guān)鍵字段,所有該方式返回樹結(jié)構(gòu)的 VO 都需要有該字段來”封裝自己“。
@Data public class AreaVO { /** * 子節(jié)點 list 集合 */ private List<AreaVO> childrenAreaVOList; /** * 區(qū)域id */ public Long id; /** * 地區(qū)名稱 */ public String name; /** * 所處層級 */ public Integer level; /** * 父Id */ public Long parentId; /** * 所屬省Id */ public Long provinceId; /** * 所屬地級市Id */ public Long cityId; }
2.3具體實現(xiàn)
下面同樣直接上 demo 代碼,注釋比較詳細:
@Override public List<AreaVO> getAreaStructure() { //第一步,從數(shù)據(jù)庫中查出所有數(shù)據(jù),按照排序條件進行排序,本質(zhì)上還是這個所有數(shù)據(jù)的 List 集合 List<AreaVO> areaVOList = this.findAll(Sort.by("id").descending()).stream() //注:這里使用 map 映射了需要返回的 VO,即相同的屬性字段就會轉(zhuǎn)換 .map(e -> e.copyProperties(AreaVO.class)).collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(areaVOList)){ //第二步,根據(jù)父Id 字段進行分組,即所有數(shù)據(jù)都會按照第一層至最后一層都按照父子關(guān)系進行分組;注意,是對所有數(shù)據(jù)分組 Map<Long, List<AreaVO>> areaVoListMap = areaVOList.parallelStream().collect(Collectors.groupingBy(AreaVO::getParentId)); //第三步,也是最關(guān)鍵的一步,將所有子數(shù)據(jù) List 集合經(jīng)過遍歷后都一層層地放置好,最終會得到一個包含父子關(guān)系的完整List areaVOList.forEach(e -> e.setChildrenAreaVOList(areaVoListMap.get(e.getId()))); //第四步,過濾出符合頂層父Id的所有數(shù)據(jù),即所有數(shù)據(jù)都歸屬于一個頂層父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效果展示
我這里測試環(huán)境的例子是只有省/直轄市/自治區(qū)、地級市、區(qū)/縣級市/縣這三級,數(shù)據(jù)也沒有完全展開,當然到下面的鎮(zhèn)/街道,乃至村/小組也是沒問題的。
這里總的測試數(shù)據(jù)量是幾千條,如果加上鎮(zhèn)/街道應該得有幾萬條,速度也還是是比較快的,服務器性能(主要內(nèi)存給力)好的話,基本整個請求/響應(拋開網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。
中國行政區(qū)域信息層次結(jié)構(gòu)效果
時間消耗,這里響應只有兩百多毫秒,如下圖的接口的性能展示:
接口性能展示
三、文章小結(jié)
使用 Stream 流組裝復雜父子樹形結(jié)構(gòu)(List 集合形式)的分享到這里就結(jié)束了,編碼沒有捷徑,都是項目實踐里出真知,一點點摸索攢經(jīng)驗。
相關(guān)文章
又又叕出BUG啦!理智分析Java NIO的ByteBuffer到底有多難用
網(wǎng)絡(luò)數(shù)據(jù)的基本單位永遠是byte,Java NIO提供ByteBuffer作為字節(jié)的容器,但該類過于復雜,有點難用.本篇文章就帶大家簡單了解一下 ,需要的朋友可以參考下2021-06-06jenkins+maven+svn自動部署和發(fā)布的詳細圖文教程
Jenkins是一個開源的、可擴展的持續(xù)集成、交付、部署的基于web界面的平臺。這篇文章主要介紹了jenkins+maven+svn自動部署和發(fā)布的詳細圖文教程,需要的朋友可以參考下2020-09-09排查Failed?to?validate?connection?com.mysql.cj.jdbc.Connec
這篇文章主要介紹了Failed?to?validate?connection?com.mysql.cj.jdbc.ConnectionImpl問題排查,具有很好的參考價值,希望對大家有所幫助2023-02-02Java實現(xiàn)學生信息管理系統(tǒng)(使用數(shù)據(jù)庫)
這篇文章主要為大家詳細介紹了Java實現(xiàn)學生信息管理系統(tǒng),使用數(shù)據(jù)庫,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01