日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式)

發(fā)布時間:2024/1/16 java 51 coder
生活随笔 收集整理的這篇文章主要介紹了 【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

目錄
  • 前言
  • 一、以部門結(jié)構(gòu)為例
    • 1.1實體
    • 1.2返回VO
    • 1.3具體實現(xiàn)
    • 1.4效果展示
  • 二、以省市縣結(jié)構(gòu)為例
    • 2.1實體
    • 2.2返回VO
    • 2.3具體實現(xiàn)
    • 2.4效果展示
  • 三、文章小結(jié)

前言

在最近的開發(fā)中,一星期內(nèi)遇到了兩個類似的需求:返回組裝好的部門樹、返回組裝好的地區(qū)信息樹,最終都需要返回 List 集合對象給前端。

于是在經(jīng)過需求分析和探索實踐后,我對于這種基于 Stream 和 List 結(jié)構(gòu)的父、子樹形結(jié)構(gòu)的操作有了新的認(rèn)識,現(xiàn)在拿出來和大家作分享交流。

一般來說完成這樣的需求大多數(shù)人會想到遞歸,但遞歸的方式弊端過于明顯:方法多次自調(diào)用效率很低、數(shù)據(jù)量大容易導(dǎo)致堆棧溢出、隨著樹深度的增加其時間復(fù)雜度會呈指數(shù)級增加等。

核心思路如下:

  • 一次數(shù)據(jù)庫查詢?nèi)繑?shù)據(jù)(幾萬條),其它全是內(nèi)存操作、性能高;

  • 同時熟練使用 stream 流操作、Lambda 表達(dá)式、Java 地址引用,完成組裝。

一、以部門結(jié)構(gòu)為例

這里的實體是放在 MySQL 里的,使用簡單的封裝好的查詢語句,這個很簡單,剩下的就是內(nèi)存操作了。

1.1實體

租戶表:租戶就是一個組織或者公司,所以每個租戶都有自己的部門。下面的表結(jié)構(gòu)我只列了一些核心的字段,其它不重要。

@Data
public class PmTenant {
    /**
     * 主鍵Id
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    /**
     * 租戶名稱
     */
    private String tenantName;
    /**
     * 租戶唯一編碼,對外暴露
     */
    private String tenantCode;
    /**
     * 租戶Id
     */
    private String tenantId;
    /**
     * 租戶狀態(tài),0可用,1禁用
     */
    private Integer status;
}

部門表:公司里都會有許多的部門,一個部門里還有部門。從最頂層公司到你所在的的部門,可能會有多達(dá)六、七層。以下同樣只展示核心字段:

@Data
public class PmDept {
    /**
     * 主鍵id
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Integer id;
    /**
     * 父部門Id
     */
    private Integer parentDeptId;
    /**
     * 部門id,全局唯一,所有系統(tǒng)用
     */
    private Integer deptId;
    /**
     * 部門名稱
     */
    private String deptName;
    /**
     * 部門所處的排序
     */
    private Integer orderNum;
    /**
     * 部門所處的層級
     */
    private Integer depth;
    /**
     * 部門狀態(tài),0可用,1刪除
     */
    private Integer status;
    /**
     * 租戶id
     */
    private String tenantId;
    /**
     * 租戶編碼
     */
    private String tenantCode;
}

1.2返回VO

這個返回的VO是給前端的,里面的子節(jié)點集合屬性 childrenNodeList ,是一個關(guān)鍵字段,所有該方式返回樹結(jié)構(gòu)的 VO 都需要有該字段來”封裝自己“。

@Data
public class DeptTreeNodeVO implements Serializable {
    /**
     * 子節(jié)點 list 集合,封裝自己
     */
    private List<DeptTreeNodeVO> childrenNodeList;
    /**
     * 部門Id
     */
    protected Integer deptId;
    /**
     * 父部門Id
     */
    protected Integer parentDeptId;
    /**
     * 部門名稱
     */
    protected String deptName;
}

1.3具體實現(xiàn)

下面直接上代碼,注釋已經(jīng)說的比較清楚了:

    @Resource
    private PmTenantService pmTenantService;
    @Resource
    private PmDeptMapper pmDeptMapper;

    @Override
    public List<DeptTreeNodeVO> assembleTree(){
        //租戶信息列表,這里是兩個租戶
        List<PmTenant> tenantList = this.pmTenantService.list();
        //step1:最外層根據(jù)租戶去組裝,有兩個租戶那么 Stream 就會遍歷組裝兩次;換句話說,如果只有一個租戶,就不需要最外層的 Stream
        List<DeptTreeNodeVO> resultList = tenantList.stream().map(tenant -> {
            //注:這里 map 只是簡單轉(zhuǎn)換了返回的對象屬性(返回需要的類型),本質(zhì)還是該租戶下的所有部門數(shù)據(jù)
            List<DeptTreeNodeVO> deptTreeNodeVOList = this.selectAllDeptByTenantCode(tenant.getTenantCode())
                    .stream().map(val -> val.convertExt(DeptTreeNodeVO.class)).collect(Collectors.toList());
            //step2:利用父節(jié)點分組,即按照該租戶下的所有部門的父Id進行分組,把所有的子節(jié)點List集合都找出來并一層層分好組
            Map<Integer, List<DeptTreeNodeVO>> listMap = deptTreeNodeVOList.parallelStream()
                    .collect(Collectors.groupingBy(DeptTreeNodeVO::getParentDeptId));
            //step3:關(guān)鍵一步,關(guān)聯(lián)上子部門,將子部門的List集合經(jīng)過遍歷一層層地放置好,最終會得到完整的部門父子關(guān)系List集合
            deptTreeNodeVOList.forEach(val -> val.setChildrenNodeList(listMap.get(val.getDeptId())));
            //step4:過濾出*部門,即所有的子部門數(shù)據(jù)都?xì)w屬于一個*父Id
            List<DeptTreeNodeVO> allChildrenList = deptTreeNodeVOList.stream()
                    .filter(val -> val.getParentDeptId().equals(NumberUtils.INTEGER_ZERO)).collect(Collectors.toList());
            //組裝最外層關(guān)于租戶需要的數(shù)據(jù),實質(zhì)已經(jīng)不是處理部門數(shù)據(jù)了
            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效果展示

我這里測試的例子是只有三層,數(shù)據(jù)也沒有完全展開,當(dāng)然五六層也是沒問題的。

只要總的部門數(shù)據(jù)量在一兩萬條以內(nèi)(啥情況部門數(shù)量會有幾萬個?部門表一般是獨立于其它表的)速度都是比較快的,服務(wù)器性能(主要內(nèi)存給力)好的話,基本整個請求/響應(yīng)(拋開網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。

部門樹結(jié)構(gòu)效果圖

二、以省市縣結(jié)構(gòu)為例

這里的實體是放在 MongoDB 里的,不熟悉 MongoDB 也不要緊,這里只需要使用一次查全量的語句。

2.1實體

全國行政區(qū)表:全國的行政區(qū)包括省/直轄市/自治區(qū)、地級市、區(qū)/縣級市/縣這三級,再往下的街道/鎮(zhèn)、以及下面的村/小組就不包含了。同樣也是只留關(guān)鍵屬性:

@Data
public class Region {
    /**
     * 區(qū)域id
     */
    @Id
    public Long id;
    /**
     * 父Id
     */
    public Long parentId;
    /**
     * 地區(qū)名稱
     */
    public String name;
    /**
     * 地區(qū)全稱
     */
    public String district;
    /**
     * 所屬省
     */
    public String province;
    /**
     * 所屬地級市
     */
    public String city;
    /**
     * 所屬省Id
     */
    public Long provinceId;
    /**
     * 所屬地級市Id
     */
    public Long cityId;
    /**
     * 所處層級
     */
    public Integer depth;
}

2.2返回VO

同樣,這個里面的子節(jié)點集合屬性 childrenRegionList,是一個關(guān)鍵字段,所有該方式返回樹結(jié)構(gòu)的 VO 都需要有該字段來”封裝自己“。

@Data
public class RegionCascadeVO extends RegionVO {
    /**
     * 子節(jié)點 list 集合
     */
    private List<RegionCascadeVO> childrenRegionList;
    /**
     * 區(qū)域id
     */
    public Long id;
    /**
     * 地區(qū)名稱
     */
    public String name;
    /**
     * 所處層級
     */
    public Integer depth;
    /**
     * 省
     */
    public String province;
    /**
     * 城市
     */
    public String city;
    /**
     * 地區(qū)全稱
     */
    public String district;
    /**
     * 父Id
     */
    public Long parentId;
    /**
     * 所屬省Id
     */
    public Long provinceId;
    /**
     * 所屬地級市Id
     */
    public Long cityId;
}

2.3具體實現(xiàn)

下面同樣直接上代碼,注釋比較詳細(xì):

    @Resource
    private RegionRepository regionRepository;

    @Override
    public List<RegionCascadeVO> quickAllTree() {
        //第一步,從數(shù)據(jù)庫中查出所有數(shù)據(jù),按照排序條件進行排序,本質(zhì)上還是這個所有數(shù)據(jù)的 List 集合
        List<RegionCascadeVO> regionCascadeVOList = this.regionRepository.findAll().stream()
                //注:這里使用 map 映射了需要返回的VO,即相同的屬性字段就會轉(zhuǎn)換
                .map(val -> val.convertExt(RegionCascadeVO.class))
                //業(yè)務(wù)需要的排序規(guī)則,使用工具來處理
                .sorted((s1, s2) -> RegionSortUtil.citySort(s1.getName(), s2.getName()))
                .sorted((s1, s2) -> RegionSortUtil.countySort(s1.getName(), s2.getName()))
                .collect(Collectors.toList());
        //第二步,根據(jù)父Id 字段進行分組,即所有數(shù)據(jù)都會按照第一層至最后一層都按照父子關(guān)系進行分組;注意,是對所有數(shù)據(jù)分組
        Map<Long, List<RegionCascadeVO>> listMap = regionCascadeVOList.parallelStream().collect(Collectors.groupingBy(RegionCascadeVO::getParentId));
        //第三步,也是最關(guān)鍵的一步,將父Id下面的所有子數(shù)據(jù)List集合,經(jīng)過遍歷后都一層層地放置好,最終會得到一個包含父子關(guān)系的完整List
        regionCascadeVOList.forEach(val -> val.setChildrenRegionList(listMap.get(val.getId())));
        //第四步,過濾出符合頂層父Id的所有數(shù)據(jù),即所有數(shù)據(jù)都?xì)w屬于一個頂層父Id
        return regionCascadeVOList.stream().filter(val -> RegionConstant.CHINA_ID.equals(val.getParentId())).collect(Collectors.toList());
    }

2.4效果展示

我這里測試環(huán)境的例子是只有省/直轄市/自治區(qū)、地級市、區(qū)/縣級市/縣這三級,數(shù)據(jù)也沒有完全展開,當(dāng)然到下面的鎮(zhèn)/街道,乃至村/小組也是沒問題的。

這里總的測試數(shù)據(jù)量是幾千條,如果加上鎮(zhèn)/街道應(yīng)該得有幾萬條,速度也還是是比較快的,服務(wù)器性能(主要內(nèi)存給力)好的話,基本整個請求/響應(yīng)(拋開網(wǎng)絡(luò)I/O消耗)可以在一秒內(nèi)完成。

中國行政區(qū)域信息層次結(jié)構(gòu)效果

時間消耗,這里響應(yīng)只有兩百多毫秒,如下圖的接口的性能展示:

接口性能展示

原因只有一個:數(shù)據(jù)庫只查一次,把查到的全部數(shù)據(jù)放內(nèi)存里,剩下的就是 Stream 的內(nèi)存操作,都是地址的引用,性能是比較高的。


三、文章小結(jié)

使用 Stream 流組裝復(fù)雜父子樹形結(jié)構(gòu)(List 集合形式)的分享到這里就結(jié)束了,編碼沒有捷徑,都是項目實踐里出真知,一點點摸索攢經(jīng)驗。

如有不足和錯誤,或者你有更好的解決思路,歡迎大家的指正和交流!

總結(jié)

以上是生活随笔為你收集整理的【Java 进阶篇】使用 Stream 流和 Lambda 组装复杂父子树形结构(List 集合形式)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。