JDFrame 是一款专为 Java 开发者设计的轻量级数据处理工具性框架,其核心价值在于用链式 API 实现 SQL 语义化操作, 本质是对 steam 流的简化、增强和语义化,从而提供更加强大的流式处理能力。
添加 Maven 依赖
<dependency>
<groupId>io.github.burukeyou</groupId>
<artifactId>jdframe</artifactId>
<version>0.2.0</version>
</dependency>
在原生 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>>
做进一步的筛选排序取前十名等操作。
在这里先介绍下我最新的点击查看专栏介绍 -》《互联网大厂面试高手心法 58 讲》专栏,原价 58元,早鸟价 10 元,现在已经涨价到 19元,满 300 人立马涨价,不要再错过。
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 即可,最终得到我们期望的数据。
需求: 统计每个学生成绩排名前 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 每个方法处理后的数据是怎么样的,是怎么生成和接收数据处理后的结果,这样你才能进一步的进行数据处理。