当前位置: 首页 > 科技观察

JavaStream的分组聚合详解

时间:2023-03-12 19:18:42 科技观察

译者|翟克评论|帮助我们分析数据。例如相加、取平均值或最大/最小值。此外,可以使用JavaStream和Collectors轻松地聚合这些字段。文档中提供了这些计算的简单示例。当然还有更复杂的聚合,比如加权平均、几何平均。此外,可能需要同时聚合多个字段。在本文中,我们将展示如何使用JavaStreams更快地解决此类问题,JavaStreams是一个允许我们高效处理大量数据的框架。假定读者对JavaStreams和Collectors类有基本的了解:问题示例给出了一个简单的示例来展示其用法。这个例子将尽可能流行,以便于推广。一个由TaxEntry实体组成的集合(tax),实体代码定义如下:publicclassTaxEntry{privateStringstate;私有字符串城市;私有intnumEntries;私人双重价格;//Constructors,getters,hashCode,equalsetc}计算一个城市的税项总数很简单:MaptotalNumEntriesByCity=taxs.stream().collect(Collectors.groupingBy(TaxEntry::getCity,Collectors.summingInt(TaxEntry::getNumEntries)));Collectors.groupingBy需要两个参数:一个做分组条件的分类函数,一个做分组流的组内聚合的收集器。这里我们使用TaxEntry::getCity作为分类标准。使用Collectors::summingInt方法来处理分组流,它返回一个Collector来汇总每个组的税收项目。如果您想进行复合分组,事情会变得有点复杂。比如上一题,求每个省市的总税项,我们先定义方法:recordStateCityGroup(Stringstate,Stringcity){}注意我们用的是Java记录,是一个A定义不可变数据类的简洁方法。Java编译器会为我们生成类的getter、setter、hashCode、equals和toString方法。这非常简单地解决了这个问题:MaptotalNumEntriesForStateCity=taxs.stream().collect(groupingBy(p->newStateCityGroup(p.getState(),p.getCity()),Collectors.summingInt(TaxEntrySimple::getNumEntries)));我们使用lambda表达式设置分类函数,新建一个StateCityGroup类来封装各省的城市。分组流的收集器与之前相同。备注:为了简洁起见,在代码示例中,我们假设Collectors类的所有方法都是静态导入的。如果你想同时做几个聚合,就变得复杂了。例如,该框架没有提供一种简单的方法来计算给定省市的税项数量与平均价格之和。为了解决这个问题,我们从前面的聚合中得到灵感,定义一条记录,将所有需要聚合的字段封装起来。recordTaxEntryAggregation(inttotalNumEntries,doubleaveragePrice){}现在,我们如何同时聚合这两个字段?即做两次streamcollection,分别找到每一个aggregation,如下代码:...(TaxEntrySimple::getPrice));返回新的TaxEntryAggregation(entries,priceAverage);})));相同,但对于分组流,我们使用Collectors::collectionAndThen进行聚合。这个函数有两个参数:我们将第一个分组流转换成一个集合(使用Collectors::toList())。我们使用lambda表达式结束函数,从上一步的集合中创建两个不同的流进行聚合,并在TaxEntryAggregation类中返回它们。如果我们想同时做更多的字段聚合,那么我们会在后续的流集合中增加流的数量。这样一来,代码就会变得低效和冗余。所以我们应该寻找更好的选择。还有一个问题。通常我们在使用Collectors类的时候,能做的聚合类型是有限的。而求和、求平均和归纳法只支持整型、长整型和双精度类型。如果我们有更复杂的类型,如BigInteger或BigDecimal怎么办?更糟糕的是,归纳法只提供最小值、最大值、计数、总和和平均值的统计信息。如果我们要进行更复杂的计算,比如加权平均或几何平均怎么办?有人会说我们可以写自定义的收集器(Collectors),但这需要对收集器的接口和对流Collector流程有深入的了解。最好直接使用Collectors类中的内置方法。在下一节中,我们将解决这些问题。复杂的多重聚合:解决上面的问题,我们写一个例子。假设我们有实体:publicclassTaxEntry{privateStringstate;私有字符串城市;私有BigDecimal率;私有BigDecimal价格;recordStateCityGroup(Stringstate,Stringcity){}//Constructors,getters,hashCode/equalsetc}我们首先要思考的是,对于每一个不同的,如何求总和税目和税率与价格的乘积(∑(税率*价格))。需要注意的一点是使用BigDecimal进行多字段聚合。与上一节一样,我们定义了一个封装聚合指标的类。recordRatePriceAggregation(intcount,BigDecimalratePrice){}对于分组后的简单聚合,一个高效的方法是Collectors::toMap。MapmapAggregation=税收。溪流()。collect(toMap(p->newStateCityGroup(p.getState(),p.getCity()),p->newRatePriceAggregation(1,p.getRate().multiply(p.getPrice())),(u1,u2)->newRatePriceAggregation(u1.count()+u2.count(),u1.ratePrice().add(u2.ratePrice()))));Collectors::toMap需要三个参数:第一个参数是一个用于生成Map键的lambda表达式。此函数创建一个StateCityGroup对象作为键。这将按元素分组。第二个参数产生Map的值。在例子中,我们创建了一个RatePriceAggregation对象,初始化:1,税率和价格的乘积。最后一个参数是二元运算符,用于合并相同键(省-市)的值。然后将数量和价格加在一起进行聚合。创建一些用于测试的数据:Listtaxes=Arrays.asList(newTaxEntry("NewYork","NYC",BigDecimal.valueOf(0.2),BigDecimal.valueOf(20.0)),newTaxEntry("NewYork","NYC",BigDecimal.valueOf(0.4),BigDecimal.valueOf(10.0)),newTaxEntry("纽约","NYC",BigDecimal.valueOf(0.6),BigDecimal.valueOf(10.0)),newTaxEntry(“佛罗里达”、“奥兰多”、BigDecimal.valueOf(0.3)、BigDecimal.valueOf(13.0)));从上图得到纽约的结果:System.out.println("NewYork:"+mapAggregation.get(newStateCityGroup("NewYork","NYC")));输出:纽约:RatePriceAggregation[count=3,ratePrice=14.00]这是一个处理多个字段和非原始数据类型(在我们的例子中是BigDecimal)分组和聚合的解决方案。但是,它的缺点是您不能对最终结果进行其他聚合,例如任何类型的平均。如果要计算的加权平均值,以及每个的所有价格的总和。我们需要先计算每个中所有税目的税率与价格的乘积之和,然后除以每个个案的税目总数n。1/n∑(费率*价格)。我们定义一个包含总价的实体类。recordTaxEntryAggregation(intcount,BigDecimalweightedAveragePrice,BigDecimaltotalPrice){}然后我们解决了上面描述的问题:MapgroupByAggregation=taxs.stream().collect(groupingBy(p->newStateCityGroup(p.getState(),p.getCity()),mapping(p->newTaxEntryAggregation(1,p.getRate().multiply(p.getPrice()),p.getPrice()),collectingAndThen(reducing(newTaxEntryAggregation(0,BigDecimal.零,BigDecimal.ZERO),(u1,u2)->newTaxEntryAggregation(u1.count()+u2.count(),u1.weightedAveragePrice().add(u2.weightedAveragePrice()),u1.totalPrice()。add(u2.totalPrice())),u->newTaxEntryAggregation(u.count(),u.weightedAveragePrice().divide(BigDecimal.valueOf(u.count()),2,RoundingMode.HALF_DOWN),u.totalPrice())))));这段代码有些复杂但是有效的解决了下面的问题详细解释:Collectors::groupingBy1。我们创建一个StateCityGroup对象,用于分组2。对于分组后的stream,我们调用Collectors::mapping方法的第一个参数,将分组后的taxclass转换为TaxEntryAggregation对象,然后初始化:数字为1、税率乘以价格,就是价格。对于后续流,我们调用Collectors::collectionAndThen方法进行排序转换。1.调用Collectors::reducing创建一个带值的TaxEntryAggregation类,以防止出现空值。Lambda表达式实现reducing方法,返回TaxEntryAggregation对象,聚合对应字段。2.Inductiveconversion,用前面reducing计算的数取平均值,返回最终的TaxEntryAggregation。这种方法不仅可以同时聚合多个字段,还可以分几个阶段进行复杂的计算。所以这是解决此类问题的简单方法。总结一下:定义一条记录,封装所有需要聚合的字段,使用Collectors::mapping初始化记录,然后使用Collectors::collectionAndThen进行二次处理和最终聚合。和上一节一样,我们可以得到纽约的聚合结果:System.out.println("聚合完成:"+groupByAggregation.get(newStateCityGroup("NewYork","NYC")));Result:Finishedaggregation:TaxEntryAggregation[count=3,weightedAveragePrice=4.67,totalPrice=40.0]备注:由于TaxEntryAggregation是Java记录,不可更改,可以使用流收集器库进行并行流计算。结论我们写了几个复杂的多字段分组聚合的例子,包括非原始数据类型的多字段聚合和跨字段聚合计算。这些表明可以通过JavaStream和CollectorsAPI以及记录集合高效地处理大量数据。译者介绍翟柯,社区编辑,目前在杭州从事软件研发工作。从事电子商务、征信等系统工作。他享受分享知识的过程,丰富自己的生活。原标题:GroupingandAggregationsWithJavaStreamsbyManuBarriola