Java 8 Lambda同时按X和Y分组

8
我希望您能翻译一下关于IT技术的内容。以下是需要翻译的文本:

我正在寻找一个Lambda来优化已经检索到的数据。 如果用户没有更改日期,我想使用Java的Lambda对结果进行分组。而且我还不熟悉Java的Lambda。

我要寻找的Lambda类似于这个查询。

select z, w, min(x), max(x), avg(x), min(y), max(y), avg(y) from table group by x, w;

1
这是否纯粹是关于使用Java 8和lambda执行此操作的问题,或者任何可以实现在内存中对JDBC (?) ResultSet进行分组的解决方案都适用于您? - Lukas Eder
1
Lukas,这个想法是只使用Java和Lambda表达式,在数据库返回结果集后,将相关对象缓存起来,用户可以更改报告格式而无需等待另一个结果集等类似事情。 - Hugo Prudente
2
这没有意义。如果你说 group by x, w,你并不是“同时”分组,而只是使用元组 (x, w) 作为键进行一次分组。在每个组内,所有条目的 (x, w) 值都相同,因此每个组的 min(x), max(x), avg(x) 将分别计算为 x, x, x。顺便说一句,如果你想要这样的聚合数据库操作,让数据库执行操作比手动获取整个表并进行聚合更有效率。 - Holger
2
虽然这仍然是一个从纯Java 8的角度看来有趣的问题,但你确定你的数据库不能在其缓存缓冲区中维护数据吗?很有可能,服务器端聚合仍然优于您尝试在Java中进行的任何“优化”,特别是因为您后面可能需要实现一些更复杂的聚合操作... - Lukas Eder
1
@Holger:我认为这是一个打字错误。查询应该写成“group by z,w”。在所有其他情况下,它都是无效的SQL(除了MySQL非严格模式)。 - Lukas Eder
显示剩余4条评论
2个回答

8

我假设你有一个对象列表,想要按照给定的分组创建一个映射表。我对你的x,y,w,z有些困惑,因此我会使用自己的字段。以下是我的做法:

interface Entry {
    String getGroup1();
    String getGroup2();
    int getIntData();
    double getDoubleData();
}

List<Entry> dataList;
Map<String, Map<String, IntSummaryStatistics>> groupedStats = 
    dataList.stream()
        .collect(Collectors.groupingBy(Entry::getGroup1,
            Collectors.groupingBy(Entry::getGroup2,
                Collectors.summarizingInt(Entry::getIntData))));

如果你想获得A、B组数据的平均值,则可以使用以下代码:

groupedStats.get("A").get("B").getAverage();

如果您想同时总结多组数据,则会变得更加复杂。您需要编写自己的包装类来累积多个统计信息。下面是一个示例,其中两个数据项都在Entry中(我将它们设为int和double以使其更有趣)。
class CompoundStats {
    private final IntSummaryStatistics intDataStats = new IntSummaryStatistics();
    private final DoubleSummaryStatistics doubleDataStats = new DoubleSummaryStatistics();

    public void add(Entry entry) {
        intDataStats.accept(entry.getIntData());
        doubleDataStats.accept(entry.getDoubleData());
    }

    public CompoundStats combine(CompoundStats other) {
        intDataStats.combine(other.intDataStats);
        doubleDataStats.combine(other.doubleDataStats);
        return this;
    }
}

然后可以使用这个类来创建您自己的收集器:

Map<String, Map<String, CompoundStats>> groupedStats = 
    dataList.stream()
        .collect(Collectors.groupingBy(Entry::getGroup1,
            Collectors.groupingBy(Entry::getGroup2,
                Collector.of(CompoundStats::new, CompoundStats::add, CompoundStats::combine))));

现在您的地图返回一个CompoundStats而不是IntSummaryStatistics:
groupedStats.get("A").get("B").getDoubleStats().getAverage();

还要注意,如果您创建一个单独的类来保存您的分组,而不是使用我上面提出的两步映射,那么这将更加整洁。如果需要,这也不是一个困难的修改。

希望这对您自己的情况有用。


1
但是问题标题写的是如何同时聚合X和Y(OP写了“如何分组”,但我确定他们的意思是“如何聚合”)。我喜欢你的简单解决方案,但你能想到一种将两个IntSummaryStatistics结合在一起的方法吗? - Lukas Eder
1
@LukasEder 没问题 - 你需要一个单独的收集器来实现这个功能,这会让代码变得比较复杂,但如果你感兴趣的话,我会在答案中加入它。 - sprinter
1
@LukasEder 我在编辑完我的答案后看了你的回答——实际上我们有相同的解决方案,只是我正在实现自己的元组。就个人而言,我觉得使用方法引用的代码更易读,但这可能仅仅是个人品味。 - sprinter
1
有趣的方法。是的,方法引用更易读。但我就是无法在我的Eclipse中编译这些可恶的东西(仍然有大量的错误)。另一方面,这都是非常低级的。应该有一种更高级、更“声明式”的SQL方式来表达这些常见的聚合场景... - Lukas Eder
3
正如您所指出的,SQL是解决SQLesque问题的良好语言! - sprinter

4
我将在这个练习中使用 jOOλ 库中的 Tuple2 类型,但如果你想避免依赖性,也可以创建自己的元组类型。
我还假设您正在使用此数据来表示:
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;
    }
}

现在您可以编写:

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(

    // This is your GROUP BY criteria
    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;
        }
    )
));

甚至更好(使用最新的jOOλ API):

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)
    )
);

地图结构现在是:
(z, w) -> (all_aggregations_of(x), all_aggregations_of(y))

调用上述地图的toString()将产生以下结果:
{
    (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})
}

现在您已经获得了所有的统计数据。

附注

当然,我不知道您的确切要求,但我怀疑您很快就需要更复杂的聚合报告,例如中位数、反向分布和各种漂亮的OLAP特性,这时您会意识到SQL只是一种更容易处理这种任务的语言。

另一方面,我们肯定会添加更多类似SQL的功能到jOOλ这个主题也激发我写一篇更详细的博客文章来描述这种方法


1
Map<Tuple2<Integer, Integer>, Tuple2<IntSummaryStatistics, IntSummaryStatistics>> map = .... - 我喜欢Java。它是一种真正简洁、没有模板代码的语言。</s> - Federico Berasategui
1
那就是类型。在其他语言中,你也有相同的类型,只是如果编译器可以推断出足够的信息,你不必总是输入它。就像Java一样,当你链接另一个方法时... - Lukas Eder
1
元组没有被包括在内,因为它们适用于值类型。 - Lovro Pandžić

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接