Java 8中如何用GroupBy实现计数?求分组统计状态的更优代码
Java 8 GroupBy 计数相关问题解答
问题1:如何在Java 8中使用GroupBy函数实现计数?
其实在Java 8的Stream API里,用Collectors.groupingBy()搭配Collectors.counting()就能轻松搞定分组计数的需求。简单来说,groupingBy()负责按你指定的规则(比如对象的某个属性)把元素分成不同的组,counting()作为“下游收集器”,专门统计每个组里的元素数量。
举个直观的例子:假设咱们有一堆水果名称,要统计每种水果出现的次数:
List<String> fruits = Arrays.asList("apple", "banana", "apple", "orange", "banana", "apple"); Map<String, Long> fruitCount = fruits.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // 输出结果就是 {apple=3, banana=2, orange=1} fruitCount.forEach((fruit, count) -> System.out.println(fruit + ": " + count));
如果是自定义对象,比如你代码里的Model,要按name属性分组计数,写法也类似:
List<Model> models = new ArrayList<>(test.values()); Map<String, Long> nameCount = models.stream() .collect(Collectors.groupingBy(Model::getName, Collectors.counting()));
这样就能快速得到每个name对应的订单总数啦。
问题2:如何结合GroupBy与计数函数处理多状态统计需求?
你的需求是按股票名称分组,统计总订单数以及INPROGRESS、CANCEL、COMPLETED三种状态各自的数量。你原来的实现虽然能跑通,但多次遍历数据流,不仅效率不高,代码也有点冗余。咱们来看看更规范、高效的实现方式:
先提个小建议:拆分职责
你的Model类既存了订单的基础信息(name、status),又存了统计数据(total、各种状态计数),有点违反单一职责原则。建议单独创建一个统计用的DTO,让每个类只干一件事:
// 专门用来存统计结果的类 class StockStatusStats { private String stockName; private long total; private long inProgress; private long cancel; private long completed; // 记得添加getter和setter方法 // 自定义toString方法,方便输出格式对齐 @Override public String toString() { return String.format("%s | %d | %d | %d | %d", stockName, total, inProgress, cancel, completed); } }
方案1:嵌套GroupBy + 转换统计对象
这种方法只需要遍历一次数据流,先按股票名称分组,每个组内再按状态分组计数,最后把状态计数的结果转换成我们需要的统计对象:
Map<String, StockStatusStats> statsMap = test.values().stream() .collect(Collectors.groupingBy( Model::getName, Collectors.collectingAndThen( // 第一步:组内按状态分组计数 Collectors.groupingBy(Model::getStatus, Collectors.counting()), // 第二步:把状态计数的Map转换成StockStatusStats statusCounts -> { StockStatusStats stats = new StockStatusStats(); // 总数就是所有状态的数量之和 stats.setTotal(statusCounts.values().stream().mapToLong(Long::longValue).sum()); // 取出各状态的数量,没有的话默认0 stats.setInProgress(statusCounts.getOrDefault("Inprogress", 0L)); stats.setCancel(statusCounts.getOrDefault("Cancel", 0L)); stats.setCompleted(statusCounts.getOrDefault("Completed", 0L)); return stats; } ) )); // 输出符合要求的结果 System.out.println("Stock | TOTAL | INPROGRESS | CANCEL | COMPLETED"); System.out.println("-----------------------------------------------"); statsMap.forEach((stockName, stats) -> { stats.setStockName(stockName); System.out.println(stats); });
方案2:自定义收集器(更灵活)
如果后续可能要新增状态或者调整统计规则,自定义收集器会更灵活。咱们可以自己实现一个收集器,在流处理过程中直接累加统计数据:
// 自定义收集器,负责累加统计每个股票的状态数据 Collector<Model, StockStatusStats, StockStatusStats> stockStatsCollector = Collector.of( // 创建一个空的统计对象 StockStatusStats::new, // 处理每个订单,更新统计数据 (stats, model) -> { stats.setTotal(stats.getTotal() + 1); switch (model.getStatus()) { case "Inprogress": stats.setInProgress(stats.getInProgress() + 1); break; case "Cancel": stats.setCancel(stats.getCancel() + 1); break; case "Completed": stats.setCompleted(stats.getCompleted() + 1); break; // 以后加新状态,直接在这里加case就行 } }, // 并行流的时候,合并两个统计对象(不用并行流也建议写上,保证兼容性) (stats1, stats2) -> { stats1.setTotal(stats1.getTotal() + stats2.getTotal()); stats1.setInProgress(stats1.getInProgress() + stats2.getInProgress()); stats1.setCancel(stats1.getCancel() + stats2.getCancel()); stats1.setCompleted(stats1.getCompleted() + stats2.getCompleted()); return stats1; } ); // 用自定义收集器分组统计 Map<String, StockStatusStats> statsMap = test.values().stream() .collect(Collectors.groupingBy(Model::getName, stockStatsCollector)); // 输出结果 System.out.println("Stock | TOTAL | INPROGRESS | CANCEL | COMPLETED"); System.out.println("-----------------------------------------------"); statsMap.forEach((stockName, stats) -> { stats.setStockName(stockName); System.out.println(stats); });
两种方案怎么选?
- 如果你现在的统计逻辑比较固定,方案1代码更简洁,用现有Collectors组合就能实现,上手快;
- 如果后续可能要扩展统计规则(比如加新状态、改统计方式),方案2的扩展性更好,只需要修改自定义收集器的累加逻辑就行。
这两种方案都只需要遍历一次数据流,比你原来多次遍历的实现效率更高,代码也更整洁好维护。
内容的提问来源于stack exchange,提问作者vijayk
相关产品推荐
相关产品推荐

