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




























