了解使用 Java Streams 解決問題的直接途徑,Java Streams 是一個允許我們快速有效地處理大量數(shù)據(jù)的框架。
當(dāng)我們對列表中的元素進(jìn)行分組時,我們可以隨后聚合分組元素的字段以執(zhí)行有意義的操作,幫助我們分析數(shù)據(jù)。一些示例是加法、平均值或最大值/最小值。這些單個字段的聚合可以使用 Java Streams 和 Collectors 輕松完成。該文檔提供了如何進(jìn)行這些類型計算的簡單示例。
但是,還有更復(fù)雜的聚合,例如加權(quán)平均值、幾何平均值。此外,可能需要同時聚合多個字段。在本文中,我們將展示使用 Java Streams 解決此類問題的直接途徑。使用這個框架使我們能夠快速有效地處理大量數(shù)據(jù)。
我們假設(shè)讀者對Java Streams和實用程序Collectors類有基本的了解。
問題布局
讓我們考慮一個簡單的例子來展示我們想要解決的問題類型。我們將使它非常通用,以便我們可以輕松地概括它。讓我們考慮TaxEntry
由以下代碼定義的實體列表:
public class TaxEntry {
private String state;
private String city;
private int numEntries;
private double price;
//Constructors, getters, hashCode, equals etc
}
計算給定城市的條目總數(shù)非常簡單:
Map<String, Integer> totalNumEntriesByCity =
taxes.stream().collect(Collectors.groupingBy(TaxEntry::getCity,
Collectors.summingInt(TaxEntry::getNumEntries)));
Collectors.summingInt(TaxEntry::getNumEntries)));
Collectors.groupingBy
接受兩個參數(shù):一個分類器函數(shù)進(jìn)行分組,一個收集器對屬于給定組的所有元素進(jìn)行下游聚合。我們TaxEntry::getCity
用作分類器函數(shù)。對于下游,我們使用Collectors::summingInt
which 返回一個Collector
總和我們?yōu)槊總€分組元素獲得的稅收條目的數(shù)量。
如果我們嘗試找到復(fù)合分組,事情會稍微復(fù)雜一些。例如,對于前面的示例,給定州和?城市的條目總數(shù)。有幾種方法可以做到這一點,但一個非常簡單的方法是首先定義:
record StateCityGroup(String state, String city) {}
請注意,我們使用的是 Javarecord
,這是一種定義不可變類的簡潔方法。此外,Java 編譯器為我們生成字段訪問器方法hashCode
、、等號和toString
實現(xiàn)。有了這個,現(xiàn)在的解決方案很簡單:
Map<StateCityGroup, Integer> totalNumEntriesForStateCity =
taxes.stream().collect(groupingBy(p -> new StateCityGroup(p.getState(), p.getCity()),
Collectors.summingInt(TaxEntrySimple::getNumEntries))
);
因為Collectors::groupingBy
我們使用 lambda 表達(dá)式設(shè)置分類器函數(shù),該表達(dá)式創(chuàng)建一個StateCityGroup
封裝每個州-城市的新記錄。下游 Collector 和之前一樣。
注意:為了簡潔起見,在代碼示例中,我們將假設(shè) Collectors 類的所有方法都是靜態(tài)導(dǎo)入的,因此我們不必顯示它們的類限定。
如果我們想同時進(jìn)行多個聚合,事情開始變得更加復(fù)雜。例如,查找給定州和城市的條目數(shù)和平均價格之和。該庫沒有為這個問題提供簡單的解決方案。
為了開始解決這個問題,我們從之前的聚合中獲取線索,并定義一個記錄來封裝所有需要聚合的字段:
record TaxEntryAggregation (int totalNumEntries, double averagePrice ) {}
現(xiàn)在,我們?nèi)绾瓮瑫r對兩個字段進(jìn)行聚合?正如以下代碼中所建議的那樣,總是有可能進(jìn)行兩次流收集以分別查找每個聚合:
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::collectingAndThen
(第 3 行)進(jìn)行聚合。這個函數(shù)有兩個參數(shù):
- 我們轉(zhuǎn)換為列表的初始分組的下載流(
Collectors::toList()
在第 3 行中使用) - Finisher 函數(shù)(第 4-9 行),我們使用 lambda 表達(dá)式從前一個列表中創(chuàng)建兩個不同的流來進(jìn)行聚合并將它們組合在一個新
TaxEntryAggregation
記錄 中返回
想象一下,我們想同時進(jìn)行更多的字段聚合。我們需要相應(yīng)地增加下游列表中的流數(shù)量。代碼變得效率低下、重復(fù)性非常高且不太理想。我們應(yīng)該尋找更好的替代品。
此外,問題還不止于此,一般來說,我們受限于可以使用 Collectors 輔助類進(jìn)行的聚合類型。他們的方法 summing*、averaging* 和 summarizing* 僅支持整數(shù)、長整數(shù)和雙精度本機(jī)類型。如果我們有更復(fù)雜的類型,比如BigInteger
or ,我們該怎么辦BigDecimal
?
雪上加霜的是,summarizing* 方法僅提供 min、max、count、sum 和 average 的匯總統(tǒng)計數(shù)據(jù)。如果我們想要執(zhí)行更復(fù)雜的計算,例如加權(quán)平均值或幾何平均值怎么辦?
有些人會爭辯說我們總是可以編寫自定義收集器,但這需要了解收集器接口并很好地理解流收集器流程。使用 Collectors 類中的實用方法提供的內(nèi)置收集器更直接。在下一節(jié)中,我們將展示一些關(guān)于如何實現(xiàn)此目的的策略。
復(fù)雜的多重聚合:解決路徑
讓我們考慮一個簡單的例子,它將突出我們在上一節(jié)中提到的挑戰(zhàn)。假設(shè)我們有以下實體:
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
}
我們首先詢問每個不同的州-城市對如何找到條目的總數(shù)以及rate
與price
(∑(rate * price)) 的乘積的總和。請注意,我們正在使用BigDecimal
.
正如我們在上一節(jié)中所做的那樣,我們定義了一個封裝聚合的類:
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
2 行)接受三個參數(shù),我們執(zhí)行以下實現(xiàn):
- 第一個參數(shù)是一個 lambda 表達(dá)式,用于生成地圖的鍵。此函數(shù)創(chuàng)建
StateCityGroup
為地圖的鍵。這將按州和城市對元素進(jìn)行分組(第 2 行)。 - 第二個參數(shù)產(chǎn)生地圖的值。在我們的例子中,我們創(chuàng)建了
RatePriceAggregation
一個計數(shù)為 1 以及 rate 和 price 的乘積的初始化(第 3 行)。 - 最后,最后一個參數(shù)是
BinaryOperator
用于合并多個元素映射到同一個州-城市鍵的情況。我們將計數(shù)和價格相加以進(jìn)行匯總(第 4 行)。
讓我們演示如何設(shè)置一些示例數(shù)據(jù):
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)));
從前面的代碼示例中獲取紐約的結(jié)果很簡單:
System.out.println("New York: " + mapAggregation.get(new StateCityGroup("New York", "NYC")));
這打?。?/p>
New York: RatePriceAggregation[count=3, ratePrice=14.00]
這是一個直接的實現(xiàn),它決定了多個字段和非原始數(shù)據(jù)類型(BigDecimal
在我們的例子中)的分組和聚合。但是,它的缺點是它沒有任何終結(jié)器允許您執(zhí)行額外的操作。例如,你不能做任何類型的平均值。
為了展示這個問題,讓我們考慮一個更復(fù)雜的問題。假設(shè)我們想要找到費率-價格的加權(quán)平均值,以及每個州和城市對的所有價格的總和。特別是,要找到加權(quán)平均值,我們需要計算屬于每個州-城市對的所有條目的費率和價格的乘積之和,然后除以每個案例的條目總數(shù) n: 1/n ∑(費率 * 價格)。
為了解決這個問題,我們開始定義一個包含聚合的記錄:
record TaxEntryAggregation(int count, BigDecimal weightedAveragePrice, BigDecimal totalPrice) {}
有了這個,我們可以進(jìn)行以下實現(xiàn):
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())
)
)
));
我們可以看到代碼稍微復(fù)雜一些,但可以讓我們得到我們正在尋找的解決方案。我們將更詳細(xì)地關(guān)注它:
-
Collectors::groupingBy
(第 2 行):
- 對于分類功能,我們創(chuàng)建一個
StateCityGroup?
記錄 - 對于下游,我們調(diào)用
Collectors::mapping
(第 3 行):
- 對于第一個參數(shù),我們應(yīng)用于輸入元素的映射器將分組的州-城市稅收記錄轉(zhuǎn)換為
TaxEntryAggregation
將初始計數(shù)分配為 1 的新條目,將稅率乘以價格,然后設(shè)置價格(第 3 行)。 - 對于下游,我們調(diào)用
Collectors::collectingAndThen
(第 4 行),正如我們將看到的,這將允許我們對下游收集器應(yīng)用一個完成轉(zhuǎn)換。- 調(diào)用
Collectors::reducing
(第 4 行)
- 調(diào)用
- 創(chuàng)建一個默認(rèn)值
TaxEntryAggregation?
以涵蓋沒有下游元素的情況(第 4 行)。 - Lambda 表達(dá)式進(jìn)行歸約并返回一個
TaxEntryAggregation
包含字段聚合的新表達(dá)式(第 5、6 7 行)
- 使用在先前歸約中計算的計數(shù)執(zhí)行完成轉(zhuǎn)換,計算平均值并返回最終結(jié)果
TaxEntryAggregation
(第 9、10、11 行)。
我們看到這種實現(xiàn)不僅允許我們同時進(jìn)行多個字段聚合,而且還可以在多個階段執(zhí)行復(fù)雜的計算。
這可以很容易地推廣到解決更復(fù)雜的問題。路徑很簡單:定義一條記錄,封裝所有需要聚合的字段,Collectors::mapping
用來初始化記錄,然后申請Collectors::collectingAndThen
做歸約和最終聚合。
和以前一樣,我們可以獲得紐約的聚合:
System.out.println("Finished aggregation: " + groupByAggregation.get(new StateCityGroup("New York", "NYC")));
我們得到結(jié)果:
Finished aggregation: TaxEntryAggregation[count=3, weightedAveragePrice=4.67, totalPrice=40.0]
還值得指出的是,由于TaxEntryAggregation
是 Java?record
,它是不可變的,因此可以使用流收集器庫提供的??支持來并行計算。文章來源:http://www.zghlxwxcb.cn/news/detail-732724.html
結(jié)論
我們已經(jīng)展示了幾種策略來使用聚合進(jìn)行復(fù)雜的多字段分組,這些聚合包括具有多字段和跨字段計算的非原始數(shù)據(jù)類型。這是一個使用 Java 流和 Collectors API 的記錄列表,因此它為我們提供了快速有效地處理大量數(shù)據(jù)的能力。文章來源地址http://www.zghlxwxcb.cn/news/detail-732724.html
到了這里,關(guān)于使用 Java 流進(jìn)行分組和聚合,高效處理大量數(shù)據(jù)不再是夢!的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!