Java 8 Stream 流又臭又長?組長推薦我使用 JDFrame 框架,太舒坦了??!
JDFrame 是一款專為 Java 開發(fā)者設(shè)計的輕量級數(shù)據(jù)處理工具性框架,其核心價值在于用鏈?zhǔn)?API 實(shí)現(xiàn) SQL 語義化操作, 本質(zhì)是對 steam 流的簡化、增強(qiáng)和語義化,從而提供更加強(qiáng)大的流式處理能力。
環(huán)境準(zhǔn)備
添加 Maven 依賴
<dependency>
    <groupId>io.github.burukeyou</groupId>
    <artifactId>jdframe</artifactId>
    <version>0.2.0</version>
</dependency>(where 過濾)SQL 式的數(shù)據(jù)篩選
在原生 stream 流中,如果我們要進(jìn)行數(shù)據(jù)篩選,只能通過 fliter 方法進(jìn)行過濾,但是 JDFrame 封裝了常用的過濾邏輯,包括范圍查詢、大于等于、in 查詢、模糊查詢等等, 能在一定程度上簡化手寫 filter 的邏輯和提升語義性, 常用的篩選如下
SDFrame.read(studentList)
        .whereBetween(Student::getAge,1,8) // 過濾年齡在[1,8]歲的
        .whereNotNull(Student::getName) // 過濾名字不為空的數(shù)據(jù), 兼容了空字符串''的判斷
        .whereGt(Student::getAge,4)    // 過濾年齡大于4歲
        .whereGe(Student::getAge,4)   // 過濾年齡大于等于3歲
        .whereIn(Student::getAge, Arrays.asList(7,9)) // 過濾年齡為7歲 或者 9歲的數(shù)據(jù)
        .whereNotIn(Student::getAge, Arrays.asList(7,9)) // 過濾年齡不為7歲 或者 9歲的數(shù)據(jù)
        .whereEq(Student::getAge,4) // 過濾年齡等于4歲的數(shù)據(jù)
        .whereNotEq(Student::getAge,4) // 過濾年齡不等于4歲的數(shù)據(jù)
        .whereLike(Student::getName,"abc") // 模糊查詢,等價于 like "%abc%"(分組聚合)學(xué)校成績排名統(tǒng)計
需求: 統(tǒng)計每個學(xué)校在 10-26 歲年齡段學(xué)生的總分?jǐn)?shù),并篩選總分 ≥800 的學(xué)校,然后取總分最多的前十名學(xué)校
如果用 SQL 實(shí)現(xiàn),我們通常會是下面這樣. 分組求和即可
select school, sum(score)
from student
where age >= 10and age <= 26and age isnotnull
groupby school
havingsum(score) >= 800
orderbysum(score) desc
limit10那同樣的,下面我們來看如何用 JDFrame 來實(shí)現(xiàn)
// 假設(shè)有以下學(xué)生數(shù)據(jù)列表
List<Student> studentList = new ArrayList<>();
// 數(shù)據(jù)處理
List<FI2<String, BigDecimal>> sdf2 = SDFrame.read(studentList) // 轉(zhuǎn)換成DataFrame模型
                .whereNotNull(Student::getAge)    // 過濾年齡不為null的
                .whereBetween(Student::getAge,10,26)   // 獲取年齡在10到26歲之間的
                .groupBySum(Student::getSchool, Student::getScore) // 按照學(xué)校分組求和計算合計分?jǐn)?shù)
                .whereGe(FI2::getC2,new BigDecimal(800)) // 過濾合計分?jǐn)?shù)大于等于800的數(shù)據(jù)
                .sortDesc(FI2::getC2) // 按照分組后的合計分?jǐn)?shù)降序排序
                .cutFirst(10)    // 截取前10名
                .toLists();     // 轉(zhuǎn)換成List拿到結(jié)果在這里我們主要用到了 JDFrame 的分組求和函數(shù)groupBySum, 它有兩個參數(shù),第一個參數(shù)表示分組的字段,也就是對學(xué)校分組,第二個參數(shù)表示求和的字段,也就是對成績求和。執(zhí)行完之后會得到一個固定的列表對象 List<FI2<String, BigDecimal>>, 用來裝載我們統(tǒng)計后的矩陣列表結(jié)果。 然后我們繼續(xù)在分組求和的基礎(chǔ)上繼續(xù) 對數(shù)據(jù)對象 List<FI2<String, BigDecimal>> 做進(jìn)一步的篩選排序取前十名等操作。
FI2 是個啥對象我們打開源碼看一下
public class FI2<T1, T2> {
    private T1 c1;
    private T2 c2;
}可以發(fā)現(xiàn)只是一個有兩個泛型字段 c1、c2 的普通對象而已。 通常用來表示存儲一行的結(jié)果。 c1 就表示存儲第一列的結(jié)果,c2 存儲第二列的結(jié)果。 類似于我們統(tǒng)計后的數(shù)據(jù)列表和 FI2 對應(yīng)關(guān)系如下
在這里插入圖片描述
可以發(fā)現(xiàn)它的整理鏈?zhǔn)秸{(diào)用語義和 SQL 語法是基本一樣的,如果讓我們用原生 stream 流或者手寫 Java 代碼是實(shí)現(xiàn),想必又是一段不少的代碼量。
(爆炸函數(shù))愛好統(tǒng)計
需求: 有以下列表數(shù)據(jù),統(tǒng)計出每個愛好有多少人喜歡,然后統(tǒng)計前 3 名的愛好, 同時過濾掉愛好有桌球的人不參與統(tǒng)計
在這里插入圖片描述
接下來我們看看用 JDFrame 如何實(shí)現(xiàn)
@Test
public void test99() {
    List<User> list = new ArrayList<>();
        list.add(new User("A","[籃球,足球,電影]"));
        list.add(new User("B","[籃球,唱歌,電影]"));
        list.add(new User("C","[足球,電影]"));
        list.add(new User("D","[羽毛球,足球]"));
        list.add(new User("E","[足球,桌球]"));
        // Map<愛好,該愛好的用戶人數(shù)>
        Map<String, Long> stats = SDFrame.read(list)
                .explodeString(User::getHobby, User::setHobby, ",") // 將愛好字段切割,變成多行
                .whereNotEq(User::getHobby, "桌球") //  過濾 愛好 不等于 桌球的用戶
                .groupByCount(User::getHobby)// 按照愛好分組,并統(tǒng)計組內(nèi)人數(shù)
                .sortDesc(FI2::getC2) // 分組后按照組內(nèi)人數(shù)排序
                .cutFirst(3)  // 取排名前3的愛好
                .toMap(FI2::getC1, FI2::getC2); // 轉(zhuǎn)換成Map輸出
}
@AllArgsConstructor
@Data
publicstaticclass User {
    private String name;
    private String hobby;
}接下來我們逐步分析下 JDFrame 是如何實(shí)現(xiàn)這一需求的
首先是調(diào)用了explodeString爆炸函數(shù), 它的作用就是會將指定的字段(User::getHoby)按照指定的分隔符(",")進(jìn)行切割, 然后將切割后每個值,重新生成一個 User 對象并添加到集合中。
如果用 excel 來表示,那么執(zhí)行explodeString函數(shù)后的數(shù)據(jù)列表如下, 原來 5 行的數(shù)據(jù)列表,經(jīng)過“炸開”后變成了 12 行
在這里插入圖片描述
類似于下面?zhèn)未a, 可以將一行數(shù)據(jù)切割后變成多行的數(shù)據(jù)
List<User> newUserList = new ArrayList<>();
        // 遍歷每個用戶
        for (User user : list) {
            // 獲取用戶愛好
            String hobby = user.getHobby();
            // 將愛好切割
            String[] hobbyArr = hobby.substring(1, hobby.length() - 1).split(",");
            //
            List<User> newList = Arrays.stream(hobbyArr).map(hb -> new User(user.getName(), hb)).collect(Collectors.toList());
            newUserList.addAll(newList);
        }
        list.addAll(newUserList);原始數(shù)據(jù)經(jīng)過炸開后,其實(shí)就非常方便可以進(jìn)行數(shù)據(jù)的精確過濾,分組統(tǒng)計和排名了。
然后 執(zhí)行 groupByCount 分組求數(shù)量函數(shù)后,數(shù)據(jù)列表如下
在這里插入圖片描述
之后我們根據(jù) c2字段(人數(shù))進(jìn)行排序然后取前 3 即可,最終得到我們期望的數(shù)據(jù)。
(窗口函數(shù))TopN 問題
需求: 統(tǒng)計每個學(xué)生成績排名前 3 的課程
如果用 SQL 實(shí)現(xiàn), 我們通常會采用下面的窗口函數(shù)實(shí)現(xiàn)
SELECT * FROM (
    SELECT *,
        DENSE_RANK() OVER(PARTITION BY 學(xué)生id ORDER BY 成績 DESC) AS ranking
    FROM 學(xué)生成績表
) t
WHERE ranking <= 3;下面我們看看如何同 JDFrame 實(shí)現(xiàn)
@Data
    @AllArgsConstructor
    @NoArgsConstructor
    publicstaticclass User {
        privateint id;
        private String name;
        private String subject;
        private Integer score;
    }
    @Test
    public void test() {
        // 構(gòu)建測試數(shù)據(jù)
        List<User> studentList = Arrays.asList(
                new User(1, "張三", "語文",90),
                new User(1, "張三","數(shù)學(xué)", 88),
                new User(1, "張三","英語", 100),
                new User(1, "張三","化學(xué)", 30),
                new User(1, "張三","物理", 20),
                new User(2, "李四", "語文",50),
                new User(2, "李四","音樂", 68),
                new User(2, "李四","英語", 90),
                new User(2, "李四","科學(xué)", 80),
                new User(2, "李四","物理", 70),
                new User(3, "王五", "歷史",12),
                new User(3, "王五","數(shù)學(xué)", 89),
                new User(3, "王五","地理", 93),
                new User(3, "王五","化學(xué)", 38),
                new User(3, "王五","物理", 69)
        );
     // 統(tǒng)計每個學(xué)生成績排名前3的課程
        List<User> lists = SDFrame.read(studentList)
                 // 根據(jù)用戶姓名進(jìn)行分組,組內(nèi)根據(jù)課程成績降序排序
                .window(Window.groupBy(User::getName).sortDesc(User::getScore))
                 // 生成組內(nèi)的排名列
                .overDenseRank()
                // 保留排名小于等于3的 成績
                .whereLe(FI2::getC2, 3)
                .map(FI2::getC1)
                .toLists();
        for (User e : lists) {
            System.out.println(e);
        }
    }最后輸出的結(jié)果如下:
User(id=2, name=李四, subject=英語, score=90)
User(id=2, name=李四, subject=科學(xué), score=80)
User(id=2, name=李四, subject=物理, score=70)
User(id=1, name=張三, subject=英語, score=100)
User(id=1, name=張三, subject=語文, score=90)
User(id=1, name=張三, subject=數(shù)學(xué), score=88)
User(id=3, name=王五, subject=地理, score=93)
User(id=3, name=王五, subject=數(shù)學(xué), score=89)
User(id=3, name=王五, subject=物理, score=69)接下來分析下代碼執(zhí)行過程:
首選執(zhí)行了window開窗 和 overDenseRank窗口計算函數(shù)之后,JDFrame 會把數(shù)據(jù)處理成如下圖,會先分組排序,然后組內(nèi)根據(jù)成績進(jìn)行排名,然后將生成的排名字段值列放到 FI2 類的 c2 字段進(jìn)行接收,c1 字段存放每個學(xué)生,這樣就得到了排名。 然后再執(zhí)行 .whereLe(FI2::getC2, 3) 過濾 c2 字段值為小于等于 3 的就是每個學(xué)生的排名前 3 的成績了
在這里插入圖片描述
最后
JDFrame 熟練后能在一定程度上提升我們處理數(shù)據(jù)的效率和可讀性, 但是需要使用者有矩陣計算的思想, 以及需要掌握 JDFrame 每個方法處理后的數(shù)據(jù)是怎么樣的,是怎么生成和接收數(shù)據(jù)處理后的結(jié)果,這樣你才能進(jìn)一步的進(jìn)行數(shù)據(jù)處理。















 
 
 










 
 
 
 