从 stream 到 kotlin 再到 SPL
在数据库外的结构化数据计算方面,Stream 迈出了从无到有的一步;Kotlin 稍稍加强了这种能力,但编译性语言的特性使它无法走得更远;要想真正解决库外结构化数据计算的难题,还需要 SPL 这种专业的结构化数据计算语言。点击从 stream 到 kotlin 再到 SPL了解详情。
Java开发中经常会遇到不方便使用数据库但又要结构化数据计算的情况。在很长一段时间里,JAVA没有提供类库去处理这种情况,即使排序、分组这类基本计算都要开发者自己从底层开始硬编码,正常的业务逻辑就更难实现了。直到JAVA8推出了Stream类库,库外结构化数据计算的难题终于得以初步解决。
下面,让我们用几个例子,重温一下Stream的数据计算能力。
排序:订单表有OrderID、Client、SellerId、Amount、OrderDate等字段,先对Client字段逆序排序,再对Amount字段顺序排序。Stream的关键代码如下
record Order(int OrderID, String Client, int SellerId, double Amount, Date OrderDate) {} Stream<Order> Orders=….. //生成订单表,省略取数过程 Stream<Order> result=Orders .sorted((sAmount1,sAmount2)->Double.compare(sAmount1.Amount,sAmount2.Amount)) .sorted((sClient1,sClient2)->CharSequence.compare(sClient2.Client,sClient1.Client)); |
对结构化数据类型Stream<Order>使用函数sorted可进行排序,函数compare可比较大小并返回真假。注意,两个sorted函数连用的方式称为流式编程, sorted的参数使用了匿名函数,即lambda语法,这是一种函数式编程。代码中排序的字段要前后颠倒,才能符合业务上的排序顺序。
分组汇总:对订单表的年份和Client分组,组内对Amount求和并计数。关键代码如下:
Calendar cal=Calendar.getInstance(); |
对订单表用groupingBy函数,并辅以collect、Collectors、summarizingDouble、DoubleSummaryStatistics等类和函数可实现分组汇总。注意,分组汇总的结果不再是结构化数据对象,而是Map对象。函数grouping只支持一个分组变量,可以用record类型将两个分组字段合并为一个分组变量,但代码会变复杂,为简化代码,这里将两个字段用下划线合并为一个字符串。
关联计算:员工表的主要字段有EId、Dept、Gender等,其中EId是订单表的SellerId字段的逻辑外键,对两表进行内关联,然后对员工表的Dept字段进行分组,对订单的Amount字段求和。
Map<Integer, Employee> EIds = Employees.collect(Collectors.toMap(Employee::EId, Function.identity())); |
Stream不直接支持关联计算,所以先计算出员工ID和员工记录的对应关系,再将订单表中的SellerId替换成员工记录;最后对订单表进行分组汇总。注意,原订单表的EId是整数型,替换成记录后数据类型不同,要生成新的订单表OrderRelation。需要过滤掉新订单表里SellerId为空的记录,以符合内关联的定义。计算结果不再是结构化对象,而是Map对象。
从上面几个例子可以看到Stream在结构化数据计算方面的优点:有一定计算能力,可以提高一定的开发效率。具体来说,Stream提供了一些基本计算函数,遇到相应的题目时不必再硬编码,代码长度显著缩短;提供了lamdb语法这种简单的函数式编程,使自定义计算函数的写法明显简化;提供了流式编程,使多步骤计算变得容易。
Stream虽然做出了突破性的贡献,但缺点也不容忽视,最致命的缺点就是计算能力不足。Stream的中间计算结果和最终结果都要事先定义,而结构的定义和赋值都很麻烦,比如例子中的新订单,为了简化代码可以不定义而直接用Map,但阅读和使用又不直观。Stream虽然支持lambda语法,但接口规则比较复杂,代码没短多少阅读障碍却显著增加。Stream的结构化对象如record\entiry\Map都不方便,必须用“对象x.单价*x. 数量”来表达,不能省略对象名,简单地用“单价*数量”来表达。
Stream的计算能力不足,根本原因就在于JAVA缺乏专业的结构化数据对象,缺少来自底层的有力支持。JAVA是编译型语言,返回值的结构必须事先定义,不能像解释性语言天然就支持动态结构。JAVA必须用一套复杂的规则来实现lambda语法,不能像解释性语言可方便地将参数表达式指定为值参数或函数参数。JAVA的结构化数据对象不够专业,这一点在其他地方也多有表现,比如不支持省略数据对象名而直接引用字段;缺乏一些基本函数比如各种关联计算和集合计算;即使已经支持的基本函数,也需要多个函数辅助才能完成计算,比如分组汇总;即使看上去最简单的计算,也存在用法古怪的问题,比如多字段排序。
Stream计算能力不足,但库外计算的需求不会消失,挑战者因此层出不穷,其中Kotlin尤为突出。Kotlin是一门全兼容JAVA生态系统、并额外支持JavaScript的开发语言,它以Stream为基础做出了重大改进,计算能力进一步提升,以至于被戏称为JAVA最重要的第三方类库。
下面,让我们用同样的例子,体会一下Kotlin在结构化数据计算方面做出的改进。
排序:
data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date) var Orders:List<Order> =…..//生成订单表,省略取数过程 var resutl=Orders.sortedBy{it.Amount}.sortedByDescending{it.Client} |
对结构化数据类型List<Order>使用函数sortedBy可实现排序,不必用其他函数进行辅助计算。注意,排序字段要前后颠倒。
分组汇总:
data class Grp(var OrderYear:Int,var SellerId:Int) .toSortedMap(compareBy<Grp> { it. OrderYear}.thenBy {it. SellerId}) |
用函数groupingBy执行分组,用函数fold执行汇总。注意,汇总之后的排序是为了和SQL保持结果一致,不是必须步骤。计算结果是Map类型,不再是结构化类型(data class)。分组字段用的是结构化类型,虽然要事先定义结构,但使用时比较方便,也可以将两个分组字段拼合到一起,虽然不必事先定义结构,但代码更加复杂。
关联计算:
data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date) var result1=result.groupingBy{it!!.SellerId.Dept} .fold(Agg(0.0,0),{ acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1) }).toSortedMap() |
Kotlin不直接支持关联,所以先循环订单,将SellerId替换为员工记录,以便间接实现关联计算,最后执行分组汇总。注意,Kotlin可以根据ID方便地找到记录,不必事先准备员工ID和员工记录的对应关系。原订单表的EId是整数型,替换成记录后数据类型不同,必须生成新的订单表。需要过滤掉新订单表里SellerId为空的记录,以符合内关联的定义。计算结果不再是结构化对象,而是Map对象。
通过例子可以看到,Kotlin的确做出了改进,比Stream的计算能力更强。具体来说,Kotlin的Lambda语法更加简洁,代码更加简短;有些基本计算函数也得到完善,不需要其他函数辅助就能完成计算,比如排序;补充了一些基本计算函数,比如交集、并集、补集。
但是,Kotlin只做出了微弱的改进,计算能力还是严重不足。Kotlin的中间计算结果和最终结果仍然要事先定义,并不能在计算中动态生成。Kotlin的lambda语法还是难以阅读,远不如SQL易懂。Kotlin的结构化对象计算时仍然不能省略对象名,简单地用“单价*数量”来表达。事实上,这些都是Stream原本就存在的问题。
与Stream一样,Kotlin的计算能力不足,它也缺乏专业的结构化数据对象,无法支持动态数据结构,难以真正简化Lambda语法,无法直接引用字段。Kotlin仍然缺乏一些重要的基本函数,比如关联计算,开发者仍然要硬编码完成计算,对于多个基本计算组合而成的业务算法,开发过程仍然困难。
Kotlin的计算能力受限于编译性语言这块天花板,如果开发者需要更专业的库外计算能力,还能选用哪些工具呢?
集算器 SPL是个可靠的选择。
集算器 SPL是专业的开源结构化数据计算语言,内置丰富的计算函数,有完善的结构化数据对象,提供了不依赖于数据库的结构化数据计算能力。对于前面列出的运算,SPL写起来简单多了。
排序:
A |
|
1 |
=Orders=file("Orders.txt").import@t() |
2 |
=Orders.sort(-Client, Amount) |
对结构化数据类型序表使用函数sort可实现排序,不必用其他函数辅助计算,也无须前后颠倒字段。
分组汇总:
=Orders.groups(year(OrderDate),Client; sum(Amount)) |
使用函数groups进行分组汇总,不必使用其他函数辅助计算。计算结果同样是序表,无须事先定义,
关联计算:
=join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount)) |
先用join函数实现内关联,再进行分组汇总。中间结果和最终结果都是序表,无须事先定义。只需稍作改动就可以切换关联类型,比如join@1表示左关联,join@f表示全关联。
SPL提供了通用的JDBC接口,这些SPL代码很容易像嵌入JAVA中执行(类似SQL),或以脚本文件的形式被JAVA调用(类似存储过程)。
… Class.forName("com.esproc.jdbc.InternalDriver"); ResultSet result = statement.executeQuery("=file(\"Orders.txt\").import@t().sort(-Client, Amount)"); //result = statement.executeQuery("call splFileName(?)"); ... |
更多详情参考官方文档,这里不再详细展开。
实际上,SPL的计算能力还远远超过SQL。比如下面这些较复杂的例子,用SQL很麻烦,但用SPL就容易多了。
连续值班:Duty.xlsx记录着每日值班情况,一个人通常会持续值班几个工作日,之后再换人,现在要根据duty依次计算出每个人连续的值班情况。处理前后的部分数据如下:
处理前(Duty.xlsx)
Date |
Name |
2018-03-01 |
Emily |
2018-03-02 |
Emily |
2018-03-04 |
Emily |
2018-03-04 |
Johnson |
2018-04-05 |
Ashley |
2018-03-06 |
Emily |
2018-03-07 |
Emily |
… |
… |
处理后
Name |
Begin |
End |
Emily |
2018-03-01 |
2018-03-03 |
Johnson |
2018-03-04 |
2018-03-04 |
Ashley |
2018-03-05 |
2018-03-05 |
Emily |
2018-03-06 |
2018-03-07 |
… |
… |
… |
SQL不擅长处理有序分组问题,要用窗口函数做嵌套子查询,很困难。而SPL提供了有序分组函数,关键代码只要一句。
A |
|
1 |
=T("D:/data/Duty.xlsx") |
2 |
=A1.group@o(name) |
3 |
=A2.new(name,~.m(1).date:begin,~.m(-1).date:end) |
找出大客户:库表sales存储客户的销售额数据,主要字段有客户client、销售额amount,找出销售额累计占到一半的前n个大客户,并按销售额从大到小排序。遇到此类较复杂的计算,SPL通常比SQL更方便,代码如下:
A |
B |
|
1 |
=demo.query(“select client,amount from sales”).sort(amount:-1) |
取数并逆序排序 |
2 |
=A1.cumulate(amount) |
计算累计序列 |
3 |
=A2.m(-1)/2 |
最后的累计值即是总和 |
4 |
=A2.pselect(~>=A3) |
超过一半的位置 |
5 |
=A1(to(A4)) |
按位置取值 |
在数据库外的计算方面,Stream迈出了从无到有的关键一步;Kotlin稍稍加强了这种能力,但编译性语言的特性使它无法走得更远;要想真正解决库外结构化数据计算的难题,还需要SPL这种专业的结构化数据计算语言。
英文版