我无法抗拒。 我已经阅读了Hugo Prudente在Stack Overflow上提出的问题 。 而且我知道必须有比JDK提供的更好的方法。
问题如下:
我正在寻找一个lambda来优化已检索的数据。 我有一个原始的结果集,如果用户不更改我想要的日期,则使用Java的lambda来对结果进行分组。 我对使用Java的lambdas还是陌生的。
我正在寻找的lambda与此查询类似的作品。
SELECTz, w, MIN(x), MAX(x), AVG(x), MIN(y), MAX(y), AVG(y) FROM table GROUP BY z, w;
函数编程不是。
在进行讨论之前,让我们建立一个非常重要的事实。 SQL是一种完全声明性的语言。 Java 8之类的功能性(或“功能性”语言,以使Haskell爱好者保持和平)不是声明性的。 尽管使用函数来表达数据转换算法要比使用对象来表达更为简洁,或更糟糕的是使用命令式指令来表达它们,但您仍在明确地表达算法。
编写SQL时,您不会编写任何算法。 您只需描述您想要的结果。 SQL引擎的优化程序将为您找出算法-例如,基于您可能在Z
但在W
或(Z, W)
上没有索引的事实。
尽管可以使用Java 8轻松实现此类简单示例,但一旦需要进行更复杂的报告,您将很快遇到Java的局限性。
当然,正如我们之前写过的,将SQL和函数式编程结合起来可以达到最佳效果 。
如何用Java 8编写?
有多种方法可以做到这一点。 本质是要了解这种转变中的所有参与者。 而且,不管您发现这是简单还是困难(适合Java 8或不足),思考新Stream API的不同,鲜为人知的部分无疑都是值得的。
这里的主要参与者是:
- Stream :如果您使用的是JDK 8库,那么新的
java.util.stream.Stream
类型将是您的首选。 - 收集器 :JDK为我们提供了一个相当低层的,因此非常强大的新API,用于数据聚合(也称为“缩减”)。 该API由新的
java.util.stream.Collector
类型进行了总结,到目前为止,我们在Blogosphere中仅听到了很少的新类型
免责声明
这里显示的某些代码可能无法在您喜欢的IDE中使用。 不幸的是,即使Java 7寿终正寝,所有主要的IDE(Eclipse,IntelliJ,NetBeans),甚至javac编译器仍然存在很多与泛型类型推断和lambda表达式组合有关的错误。 敬请期待,直到修复了这些错误! 并报告您发现的任何错误。 我们都会感谢您!
我们走吧!
让我们回顾一下我们的SQL语句:
SELECTz, w, MIN(x), MAX(x), AVG(x), MIN(y), MAX(y), AVG(y)
FROM table
GROUP BY z, w;
就Stream
API而言,表本身就是Stream
。 让我们假设我们有一个“表类型” A
:
class A {final int w;final int x;final int y;final int z;A(int w, int x, int y, int z) {this.w = w;this.x = x;this.y = y;this.z = z;}@Overridepublic String toString() {return "A{" +"w=" + w +", x=" + x +", y=" + y +", z=" + z +'}';}
}
如果需要,还可以添加equals()
和hashCode()
。
现在,我们可以使用Stream.of()
和一些示例数据轻松组成Stream
:
Stream<A> stream =
Stream.of(new A(1, 1, 1, 1),new A(1, 2, 3, 1),new A(9, 8, 6, 4),new A(9, 9, 7, 4),new A(2, 3, 4, 5),new A(2, 4, 4, 5),new A(2, 5, 5, 5));
现在,下一步是GROUP BY z, w
。 不幸的是, Stream
API本身不包含这种便捷方法。 我们必须通过指定更通用的Stream.collect()
操作,并将一个Collector
传递给它进行分组,来诉诸于更底层的操作。 幸运的是, Collectors
帮助Collectors
类中已经提供了各种不同的分组Collectors
。
因此,我们将其添加到stream
:
Stream.of(new A(1, 1, 1, 1),new A(1, 2, 3, 1),new A(9, 8, 6, 4),new A(9, 9, 7, 4),new A(2, 3, 4, 5),new A(2, 4, 4, 5),new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(...));
现在开始有趣的部分。 我们如何指定我们要同时按Az
和Aw
分组? 我们需要为该groupingBy
方法提供一个函数,该函数可以从A
类型提取诸如SQL 元组之类的东西。 我们可以编写自己的元组,也可以简单地使用jOOλ的元组, jOOλ 是我们创建并开源的库,用于改进jOOQ集成测试 。
Tuple2
类型大致如下:
public class Tuple2<T1, T2> {public final T1 v1;public final T2 v2;public T1 v1() {return v1;}public T2 v2() {return v2;}public Tuple2(T1 v1, T2 v2) {this.v1 = v1;this.v2 = v2;}
}public interface Tuple {static <T1, T2> Tuple2<T1, T2> tuple(T1 v1, T2 v2) {return new Tuple2<>(v1, v2);}
}
它具有许多有用的功能,但是这些功能对于本文而言已足够。
在旁注
为什么JDK没有附带诸如C#或Scala's的内置元组, 这让我无所适从。
没有元组的函数式编程就像没有糖的咖啡:苦涩的表情。
反正…回到正轨
因此,我们按照(Az, Aw)
元组进行分组,就像在SQL中一样
Map<Tuple2<Integer, Integer>, List<A>> map =
Stream.of(new A(1, 1, 1, 1),new A(1, 2, 3, 1),new A(9, 8, 6, 4),new A(9, 9, 7, 4),new A(2, 3, 4, 5),new A(2, 4, 4, 5),new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(a -> tuple(a.z, a.w)
));
如您所见,这将产生一个冗长但非常具有描述性的类型,一个映射包含我们的分组元组作为其键,并以收集到的表记录的列表作为其值。
运行以下语句:
map.entrySet().forEach(System.out::println);
将产生:
(1, 1)=[A{w=1, x=1, y=1, z=1}, A{w=1, x=2, y=3, z=1}]
(4, 9)=[A{w=9, x=8, y=6, z=4}, A{w=9, x=9, y=7, z=4}]
(5, 2)=[A{w=2, x=3, y=4, z=5}, A{w=2, x=4, y=4, z=5}, A{w=2, x=5, y=5, z=5}]
那已经很棒了! 实际上,它的行为类似于SQL:2011标准COLLECT()
聚合函数,该函数在Oracle 10g +中也可用
现在,我们实际上不是汇总A
记录,而是汇总x
和y
的各个值。 JDK为我们提供了两个有趣的新类型,例如java.util.IntSummaryStatistics
,可通过Collectors.summarizingInt()
从Collectors
类型再次方便使用。
附带说明
就我的口味而言,这种大锤数据聚合技术有点古怪。 JDK库被故意保留为低级和冗长的,可能是为了减小库的占用空间,或者是为了防止在5-10年内(在JDK 9和10发行之后)“可怕的”后果。 可能已经过早添加 。
同时,这个IntSummaryStatistics
全部或全都不IntSummaryStatistics
,它盲目地为您的集合聚合了这些流行的聚合值:
-
COUNT(*)
-
SUM()
-
MIN()
-
MAX()
很明显,一旦有了SUM()
和COUNT(*)
,就也有AVG() = SUM() / COUNT(*)
。 所以这将是Java方式。 IntSummaryStatistics
。
如果您想知道,SQL:2011标准指定了以下聚合函数:
AVG, MAX, MIN, SUM, EVERY, ANY, SOME, COUNT, STDDEV_POP, STDDEV_SAMP, VAR_SAMP, VAR_POP, COLLECT, FUSION, INTERSECTION, COVAR_POP, COVAR_SAMP, CORR, REGR_SLOPE, REGR_INTERCEPT, REGR_COUNT, REGR_R2, REGR_AVGX, REGR_AVGY, REGR_SXX, REGR_SYY, REGR_SXY, PERCENTILE_CONT, PERCENTILE_DISC, ARRAY_AGG
很明显,SQL中还有许多其他特定于供应商的聚合和窗口函数 。 我们已经在博客上发布了所有内容:
- 可能最酷的SQL功能:窗口函数
- 如何使用逆分布函数模拟MEDIAN()聚合函数
- 很棒的PostgreSQL 9.4 / SQL:2003 FILTER子句,用于聚合函数
- 您还不知道的真正的SQL宝石:EVERY()聚合函数
- 您真的了解SQL的GROUP BY和HAVING子句吗?
- 不要错过具有FIRST_VALUE(),LAST_VALUE(),LEAD()和LAG()的超凡SQL能力
- CUME_DIST(),一个鲜为人知的SQL宝石
的确如此, MIN, MAX, SUM, COUNT, AVG
无疑是最受欢迎的。 但是如果它们没有包含在这些默认聚合类型中,而是以一种更加可组合的方式提供,那就更好了。
反正…回到正轨
如果您想保持低水平并主要使用JDK API,则可以使用以下技术在两列上实现聚合:
Map<Tuple2<Integer, Integer>, Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map = Stream.of(new A(1, 1, 1, 1),new A(1, 2, 3, 1),new A(9, 8, 6, 4),new A(9, 9, 7, 4),new A(2, 3, 4, 5),new A(2, 4, 4, 5),new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(a -> tuple(a.z, a.w),Collector.of(// When collecting, we'll aggregate data// into two IntSummaryStatistics for x and y() -> tuple(new IntSummaryStatistics(), new IntSummaryStatistics()),// The accumulator will simply take// new t = (x, y) values(r, t) -> {r.v1.accept(t.x);r.v2.accept(t.y);},// The combiner will merge two partial// aggregations, in case this is executed// in parallel(r1, r2) -> {r1.v1.combine(r2.v1);r1.v2.combine(r2.v2);return r1;})
));map.entrySet().forEach(System.out::println);
现在上面将打印
(1, 1)=(IntSummaryStatistics{count=2, sum=3, min=1, average=1.500000, max=2}, IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3})
(4, 9)=(IntSummaryStatistics{count=2, sum=17, min=8, average=8.500000, max=9}, IntSummaryStatistics{count=2, sum=13, min=6, average=6.500000, max=7})
(5, 2)=(IntSummaryStatistics{count=3, sum=12, min=3, average=4.000000, max=5}, IntSummaryStatistics{count=3, sum=13, min=4, average=4.333333, max=5})
但是显然,没有人愿意写那么多代码。 用jOOλ可以用更少的代码来实现相同的目的
Map<Tuple2<Integer, Integer>, Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map =// Seq is like a Stream, but sequential only,
// and with more features
Seq.of(new A(1, 1, 1, 1),new A(1, 2, 3, 1),new A(9, 8, 6, 4),new A(9, 9, 7, 4),new A(2, 3, 4, 5),new A(2, 4, 4, 5),new A(2, 5, 5, 5))// Seq.groupBy() is just short for
// Stream.collect(Collectors.groupingBy(...))
.groupBy(a -> tuple(a.z, a.w),// ... because once you have tuples, // why not add tuple-collectors?Tuple.collectors(Collectors.summarizingInt(a -> a.x),Collectors.summarizingInt(a -> a.y))
));
您在上面看到的内容可能与原始的非常简单的SQL语句非常接近:
SELECTz, w, MIN(x), MAX(x), AVG(x), MIN(y), MAX(y), AVG(y)
FROM table
GROUP BY z, w;
这里有趣的部分是我们拥有所谓的“元组收集器”,这是一个Collector
,它可以针对任何程度的元组(最多8个)将数据收集到汇总结果的元组中。 这是Tuple.collectors
的代码:
// All of these generics... sheesh!
static <T, A1, A2, D1, D2> Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>>
collectors(Collector<T, A1, D1> collector1, Collector<T, A2, D2> collector2
) {return Collector.of(() -> tuple(collector1.supplier().get(), collector2.supplier().get()),(a, t) -> {collector1.accumulator().accept(a.v1, t);collector2.accumulator().accept(a.v2, t);},(a1, a2) -> tuple(collector1.combiner().apply(a1.v1, a2.v1), collector2.combiner().apply(a1.v2, a2.v2)),a -> tuple(collector1.finisher().apply(a.v1), collector2.finisher().apply(a.v2)));
}
其中Tuple2<D1, D2>
是我们从collector1
(提供D1
)和collector2
(提供D2
)派生的聚合结果类型。
而已。 大功告成!
结论
Java 8是迈向Java函数编程的第一步。 使用Streams和lambda表达式,我们已经可以完成很多工作。 但是,JDK API的级别极低,使用诸如Eclipse,IntelliJ或NetBeans之类的IDE时的体验仍然有些令人沮丧。 在撰写本文(并添加Tuple.collectors()
方法)时,我已经向不同的IDE报告了大约10个错误。 在JDK 1.8.0_40之前,某些javac编译器错误尚未修复。 换一种说法:
我只是不断地向泛滥的对象抛出泛型类型参数,直到编译器停止对我不利为止
但是,我们走的很好。 我相信JDK 9(尤其是JDK 10)将附带更多有用的API,届时上述所有内容都有望从新的值类型和泛型类型专门化中受益。
我们创建了jOOλ,将缺少的片段添加到JDK库中。 如果您想全神贯注地进行函数式编程,即当您的词汇表包含诸如monads,monoids,functors之类的时髦术语(无法抗拒)时,我们建议您完全跳过JDK的Streams和jOOλ,然后下载functionaljava 马克·佩里 ( Mark Perry)或丹尼尔·迪特里希 ( Daniel Dietrich)的 javaslang
翻译自: https://www.javacodegeeks.com/2015/01/how-to-translate-sql-group-by-and-aggregations-to-java-8.html