文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

详解Java Stream的分组和聚合

2024-12-02 06:25

关注

审校 | 孙淑娟 梁策

当我们将一个集合中的元素分组后,我们可以对分组内元素的字段进行聚合,执行有意义的操作,帮助我们分析数据。比如相加,取平均数,或最大/最小值。此外,还可以用Java Stream和Collectors轻松完成这些字段的聚合。文档中提供了这些计算的简单例子。

当然,还有更复杂的聚合,如加权平均数、几何平均数。另外,可能还需要对几个字段同时进行聚合。在这篇文章中,我们将展示如何使用 Java Stream更快地解决这类问题,这个框架使我们能够高效地处理大量数据。

假设读者已对Java Streams和Collectors类有基本的了解:

问题示例

举一个简单的例子来展示它的用途,这个例子会尽量通俗,方便概括。一个由TaxEntry实体构成的集合(税收),实体代码定义如下:

public class TaxEntry {

private String state;
private String city;
private int numEntries;
private double price;
//Constructors, getters, hashCode, equals etc
}

计算每个城市的税目总数非常简单:

Map<String, Integer> totalNumEntriesByCity = 
taxes.stream().collect(Collectors.groupingBy(TaxEntry::getCity,
Collectors.summingInt(TaxEntry::getNumEntries)));

Collectors.groupingBy需要两个参数:一个分类函数来做分组条件,一个收集器来做分组后流的组内聚合。在这我们使用TaxEntry::getCity作为分类条件。使用Collectors::summingInt方法来处理分组后的流,它返回一个Collector收集器,即对每组的税目数进行合计。

如果想要进行复合分组,事情就有点复杂了。例如,在前面的问题中,去求每个省和城市的总税目数,我们先定义方法:

record StateCityGroup(String state, String city) {}

注意,我们使用的是一个Java record,这是一种定义不可变数据类的简洁方式。Java编译器会为我们生成类的getter、setter、hashCode、equals和toString方法。这样就可以很简单地解决问题:

Map<StateCityGroup, Integer> totalNumEntriesForStateCity = 
taxes.stream().collect(groupingBy(p -> new StateCityGroup(p.getState(), p.getCity()),
Collectors.summingInt(TaxEntrySimple::getNumEntries))
);

我们使用lambda表达式来设置分类函数,创建一个新的StateCityGroup类用来封装每个省的城市。分组后流的收集器与之前一致。

备注:为了简洁起见,在代码示例中,我们假设Collectors类的所有方法都是静态导入的。

如果想同时做几个聚合,就变得复杂了。例如,找到一个给定的省和城市的税目数和平均价格的总和,框架没有提供一个简单的方法。

为了解决这个问题,我们从之前的聚合中得到启发,定义一个record,封装所有需要聚合的字段。

record TaxEntryAggregation (int totalNumEntries, double averagePrice ) {}

现在,我们该怎么同时对这两个字段进行聚合呢?那就是做两次流收集,分别找到每一个聚合,如下面代码:

Map<StateCityGroup, TaxEntryAggregation> aggregationByStateCity = taxes.stream().collect(
groupingBy(p -> new StateCityGroup(p.getState(), p.getCity()),
collectingAndThen(Collectors.toList(),
list -> {int entries = list.stream().collect(
summingInt(TaxEntrySimple::getNumEntries));
double priceAverage = list.stream().collect(
averagingDouble(TaxEntrySimple::getPrice));
return new TaxEntryAggregation(entries, priceAverage);})));

分组和以前一样,但对于分组后流,我们使用Collectors::collectionAndThen进行聚合。这个函数需要两个参数:

如果我们想同时做更多的字段聚合,那么我们将增加后续流集合中的流数量。这样代码就会变得效率低下,代码冗余。所以我们应该寻找更好的替代方案。

还有一个问题,通常我们在使用Collectors类时,可以做的聚合类型有限。而且求和、求平均和归纳只提供了对integer、long和double类型的支持。如果我们有更复杂的类型如BigInteger或BigDecimal时,该怎么办?

更糟的是,归纳方法只提供了min、max、count、sum和average的统计。如果我们想进行更复杂的计算,如加权平均数或几何平均数,怎么办?

有些人会说,我们可以编写自定义的收集器(Collectors),但这需要深刻理解收集器的接口和对流式收集器流程。不如直接使用Collectors类中的内置方法。在下一节中,我们将解决这些问题。

复杂的多重聚合:一种解决方法

针对上面的问题,我们写一个例子。假设我们有实体:

public class TaxEntry {
private String state;
private String city;
private BigDecimal rate;
private BigDecimal price;
record StateCityGroup(String state, String city) {
}
//Constructors, getters, hashCode/equals etc
}

我们首先要思考的是,对于每个不同的<省-城市>,我们如何能找到税目的总数以及税率和价格的乘积的总和(∑(税率*价格))。其中需要注意的点是使用BigDecimal进行多字段聚合。

与上一节一样,我们定义了一个封装聚合指标的类。

record RatePriceAggregation(int count, BigDecimal ratePrice) {}

对于分组后的简单聚合,一个高效的方法是Collectors::toMap。

Map<StateCityGroup, RatePriceAggregation> mapAggregation = taxes.stream().collect(
toMap(p -> new StateCityGroup(p.getState(), p.getCity()),
p -> new RatePriceAggregation(1, p.getRate().multiply(p.getPrice())),
(u1,u2) -> new RatePriceAggregation( u1.count() + u2.count(), u1.ratePrice().add(u2.ratePrice()))
));

Collectors::toMap需要三个参数:

下面造一些数据来进行测试:

List<TaxEntry> taxes = Arrays.asList(
new TaxEntry("New York", "NYC", BigDecimal.valueOf(0.2), BigDecimal.valueOf(20.0)),
new TaxEntry("New York", "NYC", BigDecimal.valueOf(0.4), BigDecimal.valueOf(10.0)),
new TaxEntry("New York", "NYC", BigDecimal.valueOf(0.6), BigDecimal.valueOf(10.0)),
new TaxEntry("Florida", "Orlando", BigDecimal.valueOf(0.3), BigDecimal.valueOf(13.0)));

从上面的map中获取纽约的结果:

System.out.println("New York: " + mapAggregation.get(new StateCityGroup("New York", "NYC")));

输出结果:

New York: RatePriceAggregation[count=3, ratePrice=14.00]

这是一种解决方法,处理了多个字段和非原始数据类型(在我们的例子中为BigDecimal)的分组和聚集。但是,它的缺点是你不能进行其他最终结果的聚合,比如不能做任何形式的平均数。

如果要计算<税率-价格>的加权平均数,以及每个<省-城市>的所有价格的总和。我们需要先计算属于每个<省-城市>的所有税目的税率和价格的乘积之和,然后除以每种情况的总税目数n。1/n ∑(费率*价格)。

我们定义一个含有总价的实体类。

record TaxEntryAggregation(int count, BigDecimal weightedAveragePrice, BigDecimal totalPrice) {}

然后我们解决上述问题:

Map<StateCityGroup, TaxEntryAggregation> groupByAggregation = taxes.stream().collect(
groupingBy(p -> new StateCityGroup(p.getState(), p.getCity()),
mapping(p -> new TaxEntryAggregation(1, p.getRate().multiply(p.getPrice()), p.getPrice()),
collectingAndThen(reducing(new TaxEntryAggregation(0, BigDecimal.ZERO, BigDecimal.ZERO),
(u1,u2) -> new TaxEntryAggregation(u1.count() + u2.count(),
u1.weightedAveragePrice().add(u2.weightedAveragePrice()),
u1.totalPrice().add(u2.totalPrice()))
),
u -> new TaxEntryAggregation(u.count(),
u.weightedAveragePrice().divide(BigDecimal.valueOf(u.count()),
2, RoundingMode.HALF_DOWN),
u.totalPrice())
)
)
));

这段代码有些复杂,但有效地解决了问题。下面详细讲解一下:

1. 我们创建一个StateCityGroup对象用于分组

2. 对于分组后流,我们调用Collectors::mapping方法

1.调用Collectors::reducing

2.归纳转换,使用前一个reducing中计算的个数计算平均数,并返回最终的TaxEntryAggregation。

这个方法不仅可以同时对多个字段进行聚合,而且还可以分几个阶段进行复杂的计算。

所以这是一个去解决这类问题的简单方法。归纳一下就是:定义一个封装了所有需要聚合的字段的record,使用Collectors::mapping来初始化记录,然后使用Collectors::collectionAndThen来做二次处理和最终聚合。

与上一节一样,我们可以得到纽约的聚合结果:

System.out.println("Finished aggregation: " + groupByAggregation.get(new StateCityGroup("New York", "NYC")));

结果:

Finished aggregation: TaxEntryAggregation[count=3, weightedAveragePrice=4.67, totalPrice=40.0]

备注:由于TaxEntryAggregation是一条Java record,且是不可改变的,所以可以使用stream collector库来并行流计算。

结论

我们编写了几个复杂的多字段分组聚合示例,其中包括非原始数据类型的多字段聚合和跨字段聚合计算。这些表明了可以通过Java Stream和Collectors API及record集合来高效的处理大量数据。

译者介绍

翟珂,51CTO社区编辑,目前在杭州从事软件研发工作,做过电商、征信等方面的系统,享受分享知识的过程,充实自己的生活。

原文Grouping and Aggregations With Java Streams,作者:Manu Barriola

来源:51CTO内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯