上周加完班打车回家,师傅问我:"这么晚才下班,程序员很辛苦吧?"
我说:"还好,就今天比较晚。"
师傅笑了笑没说话,但我心里苦啊。
事情是这样的。我们系统里有个报表模块,要从各种数据源捞数据,然后做转换、过滤、分组、汇总。光是DTO转VO,一天就得写七八次。
简单for循环
代码大概是这样:
java
List<User> users = userService.findAll();
List<UserVO> result = new ArrayList<>();
for (User user : users) {
if (user.isActive() && user.getAge() > 18) {
UserVO vo = new UserVO();
vo.setName(user.getName());
vo.setAge(user.getAge());
// 还有七八个字段要set...
result.add(vo);
}
}
代码其实没毛病,就是不太好看。
于是用到了 Java8 的 Stream。
Stream 写法
java
List<UserVO> result = users.stream()
.filter(user -> user.isActive() && user.getAge() > 18)
.map(user -> {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
return vo;
})
.collect(Collectors.toList());
看起来好多了,对吧?但这只是最简单的场景。
举例
假设有这样一个需求:
从员工列表里,按部门分组,计算每个部门的平均薪资,找出平均薪资大于5000的部门,按平均薪资从高到低排序,取前三名。
数据类长这样:
java
@Data
@AllArgsConstructor
class Employee {
String name;
String dept;
double salary;
}
用Stream实现:
java
List<Employee> employees = Arrays.asList(
new Employee("张三", "技术部", 12000),
new Employee("李四", "技术部", 9000),
new Employee("王五", "市场部", 6000),
new Employee("赵六", "市场部", 4000),
new Employee("钱七", "人事部", 3000),
new Employee("孙八", "人事部", 8000)
);
List<String> topDepts = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDept,
Collectors.averagingDouble(Employee::getSalary)
))
.entrySet().stream()
.filter(e -> e.getValue() > 5000)
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(3)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
这里就有几个让人不舒服的地方:
- 先
groupingBy,再对entrySet()流式处理,绕了一圈。 - 排序那段泛型推导,必须写
Map.Entry.<String, Double>comparingByValue(),看着就累。 - 如果后续还要加条件(比如再关联另一张表),这个链会越来越臃肿。
这种代码在数据量不大、步骤少的时候没问题,可一旦要做转换、聚合等操作时,Stream 可读性会下降。
换JDFrame来写,画风突变
先加依赖(Maven):
xml
<dependency>
<groupId>io.github.davidfantasy</groupId>
<artifactId>jdframe</artifactId>
<version>最新版本</version>
</dependency>
然后用它重写上面的逻辑:
java
DataFrame<Employee> df = JDFrame.create(employees);
List<String> topDepts = df.groupBy("dept")
.avg("salary").as("avgSalary")
.filter(row -> row.getDouble("avgSalary") > 5000)
.sortDesc("avgSalary")
.select("dept")
.head(3)
.toList(row -> row.getString("dept"));
是不是清晰的像写 SQL 一样?
groupBy("dept")→ 按部门分组avg("salary")→ 计算薪资平均值filter(...)→ 过滤平均薪资>5000sortDesc(...)→ 降序排序select("dept")→ 只取部门列head(3)→ 取前三条
每一步都对应自然语言的一个动作,没有中间集合,没有泛型推导,没有流套流。
JDFrame到底是什么?
简单说,JDFrame 是一个把集合数据当成表格来操作的工具。
对比 Stream:
- Stream:一条一条遍历,一个个对象处理。
- JDFrame :而 JDFrame 换了一种思路,它把你的
List<Employee>看成一张表,适合多步骤数据清洗、报表统计、类SQL查询。
上手JDFrame,三步就够了
1. 创建DataFrame
最常见的是从List创建:
java
DataFrame<Employee> df = JDFrame.create(employees);
也可以从CSV文件、数据库ResultSet、二维数组等创建。
2. 日常操作------像玩Excel一样玩数据
筛选行 (类似SQL的WHERE):
java
df.filter(row -> row.getInt("age") > 30);
df.filterEq("dept", "技术部"); // 等于某个值
df.filterIn("status", Arrays.asList(1,2,3)); // in查询
选择列 (类似SQL的SELECT):
java
df.select("name", "salary");
df.drop("age"); // 删除某列
新增/修改列:
java
df.withColumn("annualSalary", row -> row.getDouble("salary") * 12);
排序:
java
df.sortAsc("age");
df.sortDesc("salary");
df.sort("dept", true, "age", false); // 多列排序:dept升序,age降序
分组聚合------这是JDFrame最爽的地方:
java
df.groupBy("dept")
.count().as("count")
.sum("salary").as("total")
.avg("salary").as("avg")
.max("age").as("maxAge")
.toList();
所有聚合函数都可以链式调用,结果就是一张新的DataFrame,不用像Stream那样先groupingBy,再处理Entry。
3. 两个表格之间的连接(Join)
这是Stream最难搞的场景,但JDFrame几行搞定:
java
DataFrame<Employee> empDF = JDFrame.create(employees);
DataFrame<Dept> deptDF = JDFrame.create(depts);
DataFrame<?> joined = empDF.innerJoin(deptDF, "deptId", "id")
.select("emp.name", "dept.deptName", "emp.salary");
innerJoin、leftJoin、rightJoin、fullJoin都有,连接条件支持单字段或多字段组合。
哪些地方特别适合用JDFrame?
1. 报表导出/数据统计
比如从数据库查了几万条订单记录,要在内存中按地区、品类、时间段做多层聚合,最后生成Excel。
Stream写出来基本是个屎山,而JDFrame的链式分组聚合非常时候这种场景。
2. 数据清洗/ETL
从文件读入原始数据,需要去重、转换格式、填充默认值、过滤脏数据......JDFrame的withColumn、filter、dropDuplicates一套下来,逻辑非常直观。
3. 接口数据组装
现在微服务流行,经常要从A服务查一批用户,从B服务查他们的订单,然后在内存里把两个列表关联起来,再按某种规则排序分页。
以前可能要自己维护Map<userId, List>,现在一个leftJoin搞定。
不是所有地方都适用
性能
JDFrame内部还是基于Stream+内存操作,大数据量(百万级以上)不如数据库直接聚合,适合中等规模数据。
学习成本
习惯了Stream的小伙伴刚接触列式思维,可能需要半天适应,不过一旦上手就回不去了。
生态
毕竟小众,和Spring、MyBatis没有官方集成,需要自己粘合。但这反而是优点------小巧,无侵入。
写在最后
Stream 当然是个好东西,简单的过滤映射依然是它的主场。
但当我们遇到多步骤、表关联、类SQL聚合 的场景时,可以尝试下 JDFrame。
从设计思想上看,Stream 是面向对象 + 函数式组合的思路,而 JDFrame 更偏向列式数据模型。
一个是操作对象流,一个是操作表结构。
JDFrame 的价值,不是取代 Stream,而是补充。
本文首发于公众号:程序员大华,专注前端、Java开发,AI应用和工具的分享。关注我,少走弯路,一起进步!