Stream 代码越写越难看?JDFrame 让 Java 逻辑回归优雅

上周加完班打车回家,师傅问我:"这么晚才下班,程序员很辛苦吧?"

我说:"还好,就今天比较晚。"

师傅笑了笑没说话,但我心里苦啊。

事情是这样的。我们系统里有个报表模块,要从各种数据源捞数据,然后做转换、过滤、分组、汇总。光是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());

这里就有几个让人不舒服的地方:

  1. groupingBy,再对entrySet()流式处理,绕了一圈。
  2. 排序那段泛型推导,必须写Map.Entry.<String, Double>comparingByValue(),看着就累。
  3. 如果后续还要加条件(比如再关联另一张表),这个链会越来越臃肿。

这种代码在数据量不大、步骤少的时候没问题,可一旦要做转换、聚合等操作时,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(...) → 过滤平均薪资>5000
  • sortDesc(...) → 降序排序
  • 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");

innerJoinleftJoinrightJoinfullJoin都有,连接条件支持单字段或多字段组合。


哪些地方特别适合用JDFrame?

1. 报表导出/数据统计

比如从数据库查了几万条订单记录,要在内存中按地区、品类、时间段做多层聚合,最后生成Excel。

Stream写出来基本是个屎山,而JDFrame的链式分组聚合非常时候这种场景。

2. 数据清洗/ETL

从文件读入原始数据,需要去重、转换格式、填充默认值、过滤脏数据......JDFrame的withColumnfilterdropDuplicates一套下来,逻辑非常直观。

3. 接口数据组装

现在微服务流行,经常要从A服务查一批用户,从B服务查他们的订单,然后在内存里把两个列表关联起来,再按某种规则排序分页。

以前可能要自己维护Map<userId, List>,现在一个leftJoin搞定。


不是所有地方都适用

性能

JDFrame内部还是基于Stream+内存操作,大数据量(百万级以上)不如数据库直接聚合,适合中等规模数据。

学习成本

习惯了Stream的小伙伴刚接触列式思维,可能需要半天适应,不过一旦上手就回不去了。

生态

毕竟小众,和Spring、MyBatis没有官方集成,需要自己粘合。但这反而是优点------小巧,无侵入


写在最后

Stream 当然是个好东西,简单的过滤映射依然是它的主场。

但当我们遇到多步骤、表关联、类SQL聚合 的场景时,可以尝试下 JDFrame

从设计思想上看,Stream 是面向对象 + 函数式组合的思路,而 JDFrame 更偏向列式数据模型。

一个是操作对象流,一个是操作表结构。

JDFrame 的价值,不是取代 Stream,而是补充。

本文首发于公众号:程序员大华,专注前端、Java开发,AI应用和工具的分享。关注我,少走弯路,一起进步!

相关推荐
無限進步D1 小时前
Java 运行原理
java·开发语言·入门
難釋懷1 小时前
安装Canal
java
是苏浙1 小时前
JDK17新增特性
java·开发语言
不光头强1 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp4 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多5 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood5 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员5 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai