从 stream 到 kotlin 再到 SPL

在数据库外的结构化数据计算方面,Stream 迈出了从无到有的一步;Kotlin 稍稍加强了这种能力,但编译性语言的特性使它无法走得更远;要想真正解决库外结构化数据计算的难题,还需要 SPL 这种专业的结构化数据计算语言。点击从 stream 到 kotlin 再到 SPL了解详情。

Java开发中经常会遇到不方便使用数据库但又要结构化数据计算的情况。在很长一段时间里,JAVA没有提供类库去处理这种情况,即使排序、分组这类基本计算都要开发者自己从底层开始硬编码,正常的业务逻辑就更难实现了。直到JAVA8推出了Stream类库,库外结构化数据计算的难题终于得以初步解决。

下面,让我们用几个例子,重温一下Stream的数据计算能力。

排序:订单表有OrderIDClientSellerIdAmountOrderDate等字段,先对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();
  Map<Object, DoubleSummaryStatistics> c=Orders.collect(Collectors.groupingBy(
          r->{
              cal.setTime(r.OrderDate);
              return   cal.get(Calendar.YEAR)+"_"+r.SellerId;
              },
                Collectors.summarizingDouble(r->{
                  return r.Amount;
              })
          )
  );
      for(Object sellerid:c.keySet()){
          DoubleSummaryStatistics r   =c.get(sellerid);
          String   year_sellerid[]=((String)sellerid).split("_");
          System.out.println("group   is (year):"+year_sellerid[0]+"\t (sellerid):"+year_sellerid[1]+"\t   sum is
"+r.getSum()+"\t   count is"+r.getCount());
      }

对订单表用groupingBy函数,并辅以collectCollectorssummarizingDoubleDoubleSummaryStatistics等类和函数可实现分组汇总。注意,分组汇总的结果不再是结构化数据对象,而是Map对象。函数grouping只支持一个分组变量,可以用record类型将两个分组字段合并为一个分组变量,但代码会变复杂,为简化代码,这里将两个字段用下划线合并为一个字符串。

关联计算:员工表的主要字段有EIdDeptGender等,其中EId是订单表的SellerId字段的逻辑外键,对两表进行内关联,然后对员工表的Dept字段进行分组,对订单的Amount字段求和。

Map<Integer, Employee>   EIds = Employees.collect(Collectors.toMap(Employee::EId,   Function.identity()));
  //
创建新的OrderRelation类,里面SellerId是单值,指向对应的那个Employee对象。
  record OrderRelation(int OrderID, String Client, Employee SellerId, double   Amount, Date OrderDate){}
  Stream<OrderRelation> ORS=Orders.map(r -> {
      Employee e=EIds.get(r.SellerId);
      OrderRelation or=new   OrderRelation(r.OrderID,r.Client,e,r.Amount,r.OrderDate);
      return or;
  }).filter(e->e.SellerId!=null);
  Map<String, DoubleSummaryStatistics>   c=ORS.collect(Collectors.groupingBy(r->r.SellerId.Dept,Collectors.summarizingDouble(r->r.Amount)));
  for(String dept:c.keySet()){
      DoubleSummaryStatistics r =c.get(dept);
      System.out.println("group
dept):"+dept+"     sumAmount):"+r.getSum());
  }

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)
  data class Agg(var sumAmount: Double,var rowCount:Int)
  var result=Orders.groupingBy{Grp(it.OrderDate.year+1900,it.SellerId)}
      .fold(Agg(0.0,0),{
          acc, elem ->   Agg(acc.sumAmount + elem.Amount,acc.rowCount+1)
      })

.toSortedMap(compareBy<Grp>   { it. OrderYear}.thenBy {it. SellerId})
  result.forEach{println("group fields:${it.key.OrderYear}\t${it.key.SellerId}\t   aggregate fields:${it.value.sumAmount}\t${it.value.rowCount}") }

用函数groupingBy执行分组,用函数fold执行汇总。注意,汇总之后的排序是为了和SQL保持结果一致,不是必须步骤。计算结果是Map类型,不再是结构化类型(data class)。分组字段用的是结构化类型,虽然要事先定义结构,但使用时比较方便,也可以将两个分组字段拼合到一起,虽然不必事先定义结构,但代码更加复杂。

关联计算:

data class OrderNew(var   OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var   OrderDate:Date)
  val result = Orders.map {o->var emp=Employees.firstOrNull{it.EId==o.SellerId}
      emp?.let{OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)}
      }
      .filter {o->o!=null}
  data class Agg(var sumAmount: Double,var rowCount:Int)

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的计算能力更强。具体来说,KotlinLambda语法更加简洁,代码更加简短;有些基本计算函数也得到完善,不需要其他函数辅助就能完成计算,比如排序;补充了一些基本计算函数,比如交集、并集、补集。

但是,Kotlin只做出了微弱的改进,计算能力还是严重不足Kotlin的中间计算结果和最终结果仍然要事先定义,并不能在计算中动态生成。Kotlinlambda语法还是难以阅读,远不如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");
          Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
          Statement statement =   connection.createStatement();

        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这种专业的结构化数据计算语言。

以下是广告时间

对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅



嗯,还不太了解好多乾?
猛戳这里
玩转好多乾