本教程深入探討如何使用 java stream api 處理復(fù)雜的數(shù)據(jù)聚合需求,特別是針對(duì)多條件分組、求和以及去重計(jì)數(shù)。通過構(gòu)建自定義統(tǒng)計(jì)模型和巧妙運(yùn)用 `collectors.groupingby` 結(jié)合 `collectors.reducing`,文章展示了如何高效且準(zhǔn)確地從嵌套集合中提取所需數(shù)據(jù),解決按月份統(tǒng)計(jì)總值和獨(dú)立人數(shù)的常見挑戰(zhàn)。
在進(jìn)行數(shù)據(jù)處理之前,首先需要明確數(shù)據(jù)結(jié)構(gòu)。本教程將圍繞 Person 對(duì)象進(jìn)行聚合操作,并期望將結(jié)果封裝到 DTO 對(duì)象中。為了代碼的簡(jiǎn)潔性,我們使用 Java 14 引入的 record 類型定義數(shù)據(jù)模型。
import java.time.LocalDate; import java.math.BigDecimal; // DTO中使用BigDecimal // 定義事件狀態(tài)枚舉 enum Statement { STATUS1, STATUS2, STATUS3, STATUS4 // 示例中可能包含更多狀態(tài) } // Person 記錄,代表一個(gè)事件或一個(gè)人的某次記錄 record Person(String id, Statement event, LocalDate eventDate, int value) {} // 簡(jiǎn)化 value 類型為 int // 最終結(jié)果的 DTO record DTO(int month, BigDecimal totalSum, int totalPersons) {}
初始數(shù)據(jù)可能以 Map<String, List<Person>> 的形式存在,其中鍵是 pId(Person ID),值是該 pId 對(duì)應(yīng)的 Person 對(duì)象列表。
我們的目標(biāo)是:
原始嘗試中,常見的錯(cuò)誤是直接對(duì)每個(gè) Person 記錄進(jìn)行計(jì)數(shù),導(dǎo)致 totalPersons 統(tǒng)計(jì)的是事件數(shù)量而非獨(dú)立個(gè)體數(shù)量。例如,如果 per1 在同一個(gè)月內(nèi)有兩次記錄,我們希望它只被計(jì)數(shù)一次。
立即學(xué)習(xí)“Java免費(fèi)學(xué)習(xí)筆記(深入)”;
為了同時(shí)處理求和與去重計(jì)數(shù),并確保聚合邏輯的清晰與可復(fù)用性,我們引入一個(gè)自定義的聚合器 PersonGroupMetric。這個(gè) record 將存儲(chǔ)每個(gè)分組的中間統(tǒng)計(jì)結(jié)果。
record PersonGroupMetric(int count, int sum) { // 定義一個(gè)空的度量值,作為 reducing 操作的初始值 public static final PersonGroupMetric EMPTY = new PersonGroupMetric(0, 0); // 構(gòu)造函數(shù):將一個(gè) Person 對(duì)象映射為初始的 PersonGroupMetric // 注意:此處的 count 統(tǒng)計(jì)的是事件數(shù)量,后續(xù)會(huì)處理去重 public PersonGroupMetric(Person p) { this(1, p.value()); } // 合并方法:定義如何將兩個(gè) PersonGroupMetric 實(shí)例合并 public PersonGroupMetric add(PersonGroupMetric other) { return new PersonGroupMetric( this.count + other.count, // 累加事件計(jì)數(shù) this.sum + other.sum // 累加值總和 ); } }
關(guān)鍵點(diǎn): PersonGroupMetric 的 count 字段在 add 方法中累加的是事件數(shù)量,而不是獨(dú)立人數(shù)。這是 Collectors.reducing 的一個(gè)特性,它會(huì)根據(jù) mapper 將每個(gè)元素轉(zhuǎn)換為 PersonGroupMetric,然后通過 combiner 累加。要實(shí)現(xiàn)獨(dú)立人數(shù)計(jì)數(shù),我們需要在 PersonGroupMetric 中額外存儲(chǔ) id,或者在聚合的后期進(jìn)行處理??紤]到問題描述中 Person Count 的期望結(jié)果(月1為3人,月3為2人),這暗示了我們需要對(duì) id 進(jìn)行去重。
為了實(shí)現(xiàn)按月份分組,并計(jì)算總和與去重人數(shù),我們將結(jié)合使用 Collectors.groupingBy 和 Collectors.reducing。然而,reducing 本身難以直接處理去重計(jì)數(shù)。因此,我們將采取兩步走的策略:
第一步:按月份分組,計(jì)算總和,并收集所有 Person 的 id。
import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static java.util.stream.Collectors.*; // 假設(shè) employees 是 Map<String, List<Person>> // 為了簡(jiǎn)化示例,我們直接從 List<Person> src 開始 var src = List.of( new Person("per1", Statement.STATUS1, LocalDate.of(2022, 1, 10), 1), new Person("per2", Statement.STATUS2, LocalDate.of(2022, 1, 10), 2), new Person("per3", Statement.STATUS3, LocalDate.of(2022, 1, 10), 3), new Person("per1", Statement.STATUS4, LocalDate.of(2022, 1, 10), 1), // per1 在月1有重復(fù)記錄 new Person("per1", Statement.STATUS1, LocalDate.of(2022, 2, 10), 1), new Person("per2", Statement.STATUS1, LocalDate.of(2022, 3, 10), 1), new Person("per3", Statement.STATUS2, LocalDate.of(2022, 3, 10), 2) ); // 定義一個(gè)臨時(shí)的聚合結(jié)果類,用于在中間階段存儲(chǔ)總和和所有ID record TempMonthMetric(int sum, Set<String> uniqueIds) { public static final TempMonthMetric EMPTY = new TempMonthMetric(0, ConcurrentHashMap.newKeySet()); // 使用并發(fā)Set以防并行流 public TempMonthMetric(Person p) { this(p.value(), Set.of(p.id())); } public TempMonthMetric add(TempMonthMetric other) { Set<String> combinedIds = ConcurrentHashMap.newKeySet(); combinedIds.addAll(this.uniqueIds); combinedIds.addAll(other.uniqueIds); return new TempMonthMetric(this.sum + other.sum, combinedIds); } } Map<Integer, TempMonthMetric> monthlyAggregations = src.stream() .collect(groupingBy( p -> p.eventDate().getMonthValue(), reducing( TempMonthMetric.EMPTY, TempMonthMetric::new, TempMonthMetric::add ) ));
代碼解析:
第二步:將中間結(jié)果映射到最終的 DTO。
import java.util.Comparator; import java.math.BigDecimal; List<DTO> fin = monthlyAggregations.entrySet().stream() .map(entry -> new DTO( entry.getKey(), // 月份 new BigDecimal(entry.getValue().sum()), // 總和 entry.getValue().uniqueIds().size() // 獨(dú)立人數(shù) = 集合大小 )) .sorted(Comparator.comparing(DTO::month)) // 按月份排序 .collect(toList()); // 打印結(jié)果 fin.forEach(System.out::println);
預(yù)期輸出:
DTO[month=1, totalSum=7, totalPersons=3] DTO[month=2, totalSum=1, totalPersons=1] DTO[month=3, totalSum=3, totalPersons=2]
這與問題中期望的 Person Count 結(jié)果一致。
為了方便讀者理解和運(yùn)行,以下是包含所有必要類和邏輯的完整代碼示例:
import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.math.BigDecimal; import java.util.Comparator; import static java.util.stream.Collectors.*; public class StreamAggregationTutorial { // 定義事件狀態(tài)枚舉 enum Statement { STATUS1, STATUS2, STATUS3, STATUS4 } // Person 記錄,代表一個(gè)事件或一個(gè)人的某次記錄 record Person(String id, Statement event, LocalDate eventDate, int value) {} // 最終結(jié)果的 DTO record DTO(int month, BigDecimal totalSum, int totalPersons) {} // 臨時(shí)的聚合結(jié)果類,用于在中間階段存儲(chǔ)總和和所有ID record TempMonthMetric(int sum, Set<String> uniqueIds) { // 定義一個(gè)空的度量值,作為 reducing 操作的初始值 // 使用 ConcurrentHashMap.newKeySet() 以支持并行流的安全集合操作 public static final TempMonthMetric EMPTY = new TempMonthMetric(0, ConcurrentHashMap.newKeySet()); // 構(gòu)造函數(shù):將一個(gè) Person 對(duì)象映射為初始的 TempMonthMetric public TempMonthMetric(Person p) { this(p.value(), Set.of(p.id())); } // 合并方法:定義如何將兩個(gè) TempMonthMetric 實(shí)例合并 public TempMonthMetric add(TempMonthMetric other) { Set<String> combinedIds = ConcurrentHashMap.newKeySet(); combinedIds.addAll(this.uniqueIds); combinedIds.addAll(other.uniqueIds); return new TempMonthMetric(this.sum + other.sum, combinedIds); } } public static void main(String[] args) { // 示例數(shù)據(jù) var src = List.of( new Person("per1", Statement.STATUS1, LocalDate.of(2022, 1, 10), 1), new Person("per2", Statement.STATUS2, LocalDate.of(2022, 1, 10), 2), new Person("per3", Statement.STATUS3, LocalDate.of(2022, 1, 10), 3), new Person("per1", Statement.STATUS4, LocalDate.of(2022, 1, 10), 1), // per1 在月1有重復(fù)記錄 new Person("per1", Statement.STATUS1, LocalDate.of(2022, 2, 10), 1), new Person("per2", Statement.STATUS1, LocalDate.of(2022, 3, 10), 1), new Person("per3", Statement.STATUS2, LocalDate.of(2022, 3, 10), 2) ); // 第一步:按月份分組,計(jì)算總和,并收集所有 Person 的 id Map<Integer, TempMonthMetric> monthlyAggregations = src.stream() .collect(groupingBy( p -> p.eventDate().getMonthValue(), reducing( TempMonthMetric.EMPTY, TempMonthMetric::new, TempMonthMetric::add ) )); // 第二步:將中間結(jié)果映射到最終的 DTO List<DTO> result = monthlyAggregations.entrySet().stream() .map(entry -> new DTO( entry.getKey(), // 月份 new BigDecimal(entry.getValue().sum()), // 總和 entry.getValue().uniqueIds().size() // 獨(dú)立人數(shù) = 集合大小 )) .sorted(Comparator.comparing(DTO::month)) // 按月份排序 .collect(toList()); // 打印最終結(jié)果 System.out.println("--- 最終聚合結(jié)果 ---"); result.forEach(System.out::println); } }
本教程詳細(xì)展示了如何利用 Java Stream API 的 Collectors.groupingBy 和 Collectors.reducing 來解決復(fù)雜的數(shù)據(jù)聚合問題,特別是涉及多條件分組、求和以及去重計(jì)數(shù)的需求。通過創(chuàng)建自定義的中間聚合器 (TempMonthMetric),我們能夠靈活地在流處理過程中捕獲和合并所需的所有統(tǒng)計(jì)信息,最終精確地計(jì)算出按月份分組的總和與獨(dú)立人數(shù)。這種模式對(duì)于處理類似的數(shù)據(jù)分析和報(bào)表生成任務(wù)具有很高的參考價(jià)值。
以上就是Java Stream API 高級(jí)聚合:按多條件分組計(jì)算總和與去重計(jì)數(shù)的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!
每個(gè)人都需要一臺(tái)速度更快、更穩(wěn)定的 PC。隨著時(shí)間的推移,垃圾文件、舊注冊(cè)表數(shù)據(jù)和不必要的后臺(tái)進(jìn)程會(huì)占用資源并降低性能。幸運(yùn)的是,許多工具可以讓 Windows 保持平穩(wěn)運(yùn)行。
微信掃碼
關(guān)注PHP中文網(wǎng)服務(wù)號(hào)
QQ掃碼
加入技術(shù)交流群
Copyright 2014-2025 http://ipnx.cn/ All Rights Reserved | php.cn | 湘ICP備2023035733號(hào)