【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式)
- 前言
-
一、以部門結構為例
- 1.1實體
- 1.2返回VO
- 1.3具體實現
- 1.4效果展示
-
二、以省市縣結構為例
- 2.1實體
- 2.2返回VO
- 2.3具體實現
- 2.4效果展示
- 三、文章小結
前言
在最近的開發中,一星期內遇到了兩個類似的需求:返回組裝好的部門樹、返回組裝好的地區信息樹,最終都需要返回 List 集合對象給前端。
于是在經過需求分析和探索實踐后,我對于這種基于 Stream 和 List 結構的父、子樹形結構的操作有了新的認識,現在拿出來和大家作分享交流。
一般來說完成這樣的需求大多數人會想到遞歸,但遞歸的方式弊端過于明顯:方法多次自調用效率很低、數據量大容易導致堆棧溢出、隨著樹深度的增加其時間復雜度會呈指數級增加等。
核心思路如下:
-
一次數據庫查詢全部數據(幾萬條),其它全是內存操作、性能高;
-
同時熟練使用 stream 流操作、Lambda 表達式、Java 地址引用,完成組裝。
一、以部門結構為例
這里的實體是放在 MySQL 里的,使用簡單的封裝好的查詢語句,這個很簡單,剩下的就是內存操作了。
1.1實體
租戶表:租戶就是一個組織或者公司,所以每個租戶都有自己的部門。下面的表結構我只列了一些核心的字段,其它不重要。
@Data
public class PmTenant {
/**
* 主鍵Id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 租戶名稱
*/
private String tenantName;
/**
* 租戶唯一編碼,對外暴露
*/
private String tenantCode;
/**
* 租戶Id
*/
private String tenantId;
/**
* 租戶狀態,0可用,1禁用
*/
private Integer status;
}
部門表:公司里都會有許多的部門,一個部門里還有部門。從最頂層公司到你所在的的部門,可能會有多達六、七層。以下同樣只展示核心字段:
@Data
public class PmDept {
/**
* 主鍵id
*/
@TableId(type = IdType.ASSIGN_ID)
private Integer id;
/**
* 父部門Id
*/
private Integer parentDeptId;
/**
* 部門id,全局唯一,所有系統用
*/
private Integer deptId;
/**
* 部門名稱
*/
private String deptName;
/**
* 部門所處的排序
*/
private Integer orderNum;
/**
* 部門所處的層級
*/
private Integer depth;
/**
* 部門狀態,0可用,1刪除
*/
private Integer status;
/**
* 租戶id
*/
private String tenantId;
/**
* 租戶編碼
*/
private String tenantCode;
}
1.2返回VO
這個返回的VO是給前端的,里面的子節點集合屬性 childrenNodeList ,是一個關鍵字段,所有該方式返回樹結構的 VO 都需要有該字段來”封裝自己“。
@Data
public class DeptTreeNodeVO implements Serializable {
/**
* 子節點 list 集合,封裝自己
*/
private List<DeptTreeNodeVO> childrenNodeList;
/**
* 部門Id
*/
protected Integer deptId;
/**
* 父部門Id
*/
protected Integer parentDeptId;
/**
* 部門名稱
*/
protected String deptName;
}
1.3具體實現
下面直接上代碼,注釋已經說的比較清楚了:
@Resource
private PmTenantService pmTenantService;
@Resource
private PmDeptMapper pmDeptMapper;
@Override
public List<DeptTreeNodeVO> assembleTree(){
//租戶信息列表,這里是兩個租戶
List<PmTenant> tenantList = this.pmTenantService.list();
//step1:最外層根據租戶去組裝,有兩個租戶那么 Stream 就會遍歷組裝兩次;換句話說,如果只有一個租戶,就不需要最外層的 Stream
List<DeptTreeNodeVO> resultList = tenantList.stream().map(tenant -> {
//注:這里 map 只是簡單轉換了返回的對象屬性(返回需要的類型),本質還是該租戶下的所有部門數據
List<DeptTreeNodeVO> deptTreeNodeVOList = this.selectAllDeptByTenantCode(tenant.getTenantCode())
.stream().map(val -> val.convertExt(DeptTreeNodeVO.class)).collect(Collectors.toList());
//step2:利用父節點分組,即按照該租戶下的所有部門的父Id進行分組,把所有的子節點List集合都找出來并一層層分好組
Map<Integer, List<DeptTreeNodeVO>> listMap = deptTreeNodeVOList.parallelStream()
.collect(Collectors.groupingBy(DeptTreeNodeVO::getParentDeptId));
//step3:關鍵一步,關聯上子部門,將子部門的List集合經過遍歷一層層地放置好,最終會得到完整的部門父子關系List集合
deptTreeNodeVOList.forEach(val -> val.setChildrenNodeList(listMap.get(val.getDeptId())));
//step4:過濾出*部門,即所有的子部門數據都歸屬于一個*父Id
List<DeptTreeNodeVO> allChildrenList = deptTreeNodeVOList.stream()
.filter(val -> val.getParentDeptId().equals(NumberUtils.INTEGER_ZERO)).collect(Collectors.toList());
//組裝最外層關于租戶需要的數據,實質已經不是處理部門數據了
DeptTreeNodeVO node = new DeptTreeNodeVO();
node.setChildrenNodeList(allChildrenList);
node.setDeptName(tenant.getTenantName());
return node;
}).collect(Collectors.toList());
return Optional.of(resultList).orElse(null);
}
/**
* 獲取某個租戶下的所有部門信息
*
* @return
*/
public List<PmDept> selectAllDeptByTenantCode(String tenantCode) {
return pmDeptMapper.selectList(new LambdaQueryWrapper<PmDept>()
.eq(PmDept::getTenantCode, tenantCode)
.eq(PmDept::getStatus, PmDeptStatus.DISABLE.getStatus()));
}
1.4效果展示
我這里測試的例子是只有三層,數據也沒有完全展開,當然五六層也是沒問題的。
只要總的部門數據量在一兩萬條以內(啥情況部門數量會有幾萬個?部門表一般是獨立于其它表的)速度都是比較快的,服務器性能(主要內存給力)好的話,基本整個請求/響應(拋開網絡I/O消耗)可以在一秒內完成。
二、以省市縣結構為例
這里的實體是放在 MongoDB 里的,不熟悉 MongoDB 也不要緊,這里只需要使用一次查全量的語句。
2.1實體
全國行政區表:全國的行政區包括省/直轄市/自治區、地級市、區/縣級市/縣這三級,再往下的街道/鎮、以及下面的村/小組就不包含了。同樣也是只留關鍵屬性:
@Data
public class Region {
/**
* 區域id
*/
@Id
public Long id;
/**
* 父Id
*/
public Long parentId;
/**
* 地區名稱
*/
public String name;
/**
* 地區全稱
*/
public String district;
/**
* 所屬省
*/
public String province;
/**
* 所屬地級市
*/
public String city;
/**
* 所屬省Id
*/
public Long provinceId;
/**
* 所屬地級市Id
*/
public Long cityId;
/**
* 所處層級
*/
public Integer depth;
}
2.2返回VO
同樣,這個里面的子節點集合屬性 childrenRegionList,是一個關鍵字段,所有該方式返回樹結構的 VO 都需要有該字段來”封裝自己“。
@Data
public class RegionCascadeVO extends RegionVO {
/**
* 子節點 list 集合
*/
private List<RegionCascadeVO> childrenRegionList;
/**
* 區域id
*/
public Long id;
/**
* 地區名稱
*/
public String name;
/**
* 所處層級
*/
public Integer depth;
/**
* 省
*/
public String province;
/**
* 城市
*/
public String city;
/**
* 地區全稱
*/
public String district;
/**
* 父Id
*/
public Long parentId;
/**
* 所屬省Id
*/
public Long provinceId;
/**
* 所屬地級市Id
*/
public Long cityId;
}
2.3具體實現
下面同樣直接上代碼,注釋比較詳細:
@Resource
private RegionRepository regionRepository;
@Override
public List<RegionCascadeVO> quickAllTree() {
//第一步,從數據庫中查出所有數據,按照排序條件進行排序,本質上還是這個所有數據的 List 集合
List<RegionCascadeVO> regionCascadeVOList = this.regionRepository.findAll().stream()
//注:這里使用 map 映射了需要返回的VO,即相同的屬性字段就會轉換
.map(val -> val.convertExt(RegionCascadeVO.class))
//業務需要的排序規則,使用工具來處理
.sorted((s1, s2) -> RegionSortUtil.citySort(s1.getName(), s2.getName()))
.sorted((s1, s2) -> RegionSortUtil.countySort(s1.getName(), s2.getName()))
.collect(Collectors.toList());
//第二步,根據父Id 字段進行分組,即所有數據都會按照第一層至最后一層都按照父子關系進行分組;注意,是對所有數據分組
Map<Long, List<RegionCascadeVO>> listMap = regionCascadeVOList.parallelStream().collect(Collectors.groupingBy(RegionCascadeVO::getParentId));
//第三步,也是最關鍵的一步,將父Id下面的所有子數據List集合,經過遍歷后都一層層地放置好,最終會得到一個包含父子關系的完整List
regionCascadeVOList.forEach(val -> val.setChildrenRegionList(listMap.get(val.getId())));
//第四步,過濾出符合頂層父Id的所有數據,即所有數據都歸屬于一個頂層父Id
return regionCascadeVOList.stream().filter(val -> RegionConstant.CHINA_ID.equals(val.getParentId())).collect(Collectors.toList());
}
2.4效果展示
我這里測試環境的例子是只有省/直轄市/自治區、地級市、區/縣級市/縣這三級,數據也沒有完全展開,當然到下面的鎮/街道,乃至村/小組也是沒問題的。
這里總的測試數據量是幾千條,如果加上鎮/街道應該得有幾萬條,速度也還是是比較快的,服務器性能(主要內存給力)好的話,基本整個請求/響應(拋開網絡I/O消耗)可以在一秒內完成。
時間消耗,這里響應只有兩百多毫秒,如下圖的接口的性能展示:
原因只有一個:數據庫只查一次,把查到的全部數據放內存里,剩下的就是 Stream 的內存操作,都是地址的引用,性能是比較高的。
三、文章小結
使用 Stream 流組裝復雜父子樹形結構(List 集合形式)的分享到這里就結束了,編碼沒有捷徑,都是項目實踐里出真知,一點點摸索攢經驗。
如有不足和錯誤,或者你有更好的解決思路,歡迎大家的指正和交流!
總結
以上是生活随笔為你收集整理的【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python jira 读取表数据批量新
- 下一篇: LSP 网络劫持(Layered Ser