📌 拿来即用 : Java 企业级工具类全家桶-48-分组聚合工具类
📋 目录
一、工具类功能
给业务代码用的集合统计工具:一批明细拿回来就按门店、渠道、用户分组,顺手计数、求和、算均值、取每组最大最小、把重复键压成单值,还能按笔数出榜单,业务代码不用再手写 Stream 收集器和临时累加。
结果都是保持 key 首次出现顺序的可变 Map,null 键与 null 元素都能成组;数值按 double 累加,金额请改走 BigDecimal;榜单只按计数降序,不做分页与流式聚合。
二、所需环境
JDK 17或以上
三、方法概览(按需调用)
| 序号 | 方法 | 入参 | 返回 | 用途 |
|---|---|---|---|---|
| 1 | groupBy(collection, keyExtractor) | 待分组集合 + key提取函数 | Map<K, List> | 按 key 分组,值是该组全部元素 |
| 2 | countBy(collection, keyExtractor) | 待分组集合 + key提取函数 | Map<K, Long> | 分组计数,拿每个 key 的元素个数 |
| 3 | sumBy(collection, keyExtractor, mapper) | 待分组集合 + key提取函数 + 数值提取函数 | Map<K, Double> | 按 key 对指定数值字段求和 |
| 4 | averageBy(collection, keyExtractor, mapper) | 待分组集合 + key提取函数 + 数值提取函数 | Map<K, Double> | 按 key 对指定数值字段求均值 |
| 5 | maxBy(collection, keyExtractor, comparator) | 待分组集合 + key提取函数 + 元素比较器 | Map<K, T> | 取每个 key 组内的最大元素,并列时留先出现的 |
| 6 | minBy(collection, keyExtractor, comparator) | 待分组集合 + key提取函数 + 元素比较器 | Map<K, T> | 取每个 key 组内的最小元素,并列时留先出现的 |
| 7 | toMap(collection, keyExtractor) | 待转换集合 + key提取函数 | Map<K, T> | 每个 key 只留一个元素,重复键保留后出现的 |
| 8 | sortKeysByCountDesc(countMap) | 计数Map | List | 按计数值降序出 key 榜单,计数相同的维持原顺序 |
| 9 | getGroup(groupedMap, key) | 分组Map + 要取的key | List | 安全取某一组的不可变视图,key 缺失给不可变空列表 |
四、调用示例(直观体验)
调用示例直接复制进 IDE 右键 Run 就能看到调用工具类各方法的输出结果。
java
21:00:31.351 [main] INFO -- 「GroupAggregateUtils 方法1 groupBy(collection, keyExtractor)」返回分组Map -> {望京店=[Order[shop=望京店, sku=A1, amount=38.0], Order[shop=望京店, sku=B7, amount=12.5], Order[shop=望京店, sku=A1, amount=7.5]], 国贸店=[Order[shop=国贸店, sku=A2, amount=60.0], Order[shop=国贸店, sku=C3, amount=20.0]], 西单店=[Order[shop=西单店, sku=A1, amount=99.0]]}
21:00:31.379 [main] INFO -- 「GroupAggregateUtils 方法1 groupBy(collection, keyExtractor)」null 键与 null 元素都能成组 -> {null=[Order[shop=null, sku=X1, amount=5.0]], 本条无门店=[null], 望京店=[Order[shop=望京店, sku=X2, amount=8.0]]}
21:00:31.380 [main] INFO -- 「GroupAggregateUtils 方法2 countBy(collection, keyExtractor)」返回计数Map -> {望京店=3, 国贸店=2, 西单店=1}
21:00:31.383 [main] INFO -- 「GroupAggregateUtils 方法3 sumBy(collection, keyExtractor, mapper)」返回求和Map -> {望京店=58.0, 国贸店=80.0, 西单店=99.0}
21:00:31.383 [main] INFO -- 「GroupAggregateUtils 方法4 averageBy(collection, keyExtractor, mapper)」返回均值Map -> {望京店=19.333333333333332, 国贸店=40.0, 西单店=99.0}
21:00:31.384 [main] INFO -- 「GroupAggregateUtils 方法5 maxBy(collection, keyExtractor, comparator)」返回每组最大一单 -> {望京店=Order[shop=望京店, sku=A1, amount=38.0], 国贸店=Order[shop=国贸店, sku=A2, amount=60.0], 西单店=Order[shop=西单店, sku=A1, amount=99.0]}
21:00:31.384 [main] INFO -- 「GroupAggregateUtils 方法6 minBy(collection, keyExtractor, comparator)」返回每组最小一单 -> {望京店=Order[shop=望京店, sku=A1, amount=7.5], 国贸店=Order[shop=国贸店, sku=C3, amount=20.0], 西单店=Order[shop=西单店, sku=A1, amount=99.0]}
21:00:31.385 [main] INFO -- 「GroupAggregateUtils 方法7 toMap(collection, keyExtractor)」返回key到单值Map(重复键留后出现的)-> {望京店=Order[shop=望京店, sku=A1, amount=7.5], 国贸店=Order[shop=国贸店, sku=C3, amount=20.0], 西单店=Order[shop=西单店, sku=A1, amount=99.0]}
21:00:31.385 [main] INFO -- 「GroupAggregateUtils 方法8 sortKeysByCountDesc(countMap)」返回按笔数降序的门店榜 -> [望京店, 国贸店, 西单店]
21:00:31.385 [main] INFO -- 「GroupAggregateUtils 方法9 getGroup(groupedMap, key)」返回该组不可变视图 -> [Order[shop=望京店, sku=A1, amount=38.0], Order[shop=望京店, sku=B7, amount=12.5], Order[shop=望京店, sku=A1, amount=7.5]]
21:00:31.386 [main] INFO -- 「GroupAggregateUtils 方法9 getGroup(groupedMap, key)」key 不存在时 -> []
21:00:31.386 [main] ERROR -- 「异常信息获取演示」集合入参为null -> 待分组集合不能为null
21:00:31.386 [main] ERROR -- 「异常信息获取演示」数值提取函数为null -> 数值提取函数不能为null
21:00:31.386 [main] ERROR -- 「异常信息获取演示」计数值含null -> 异常类型=NullPointerException
21:00:31.387 [main] ERROR -- 「异常信息获取演示」null元素撞上不兜底的提取器 -> 异常类型=NullPointerException
进程已结束,退出代码为 0
java
package com.example.order.api;
import com.example.common.utils.GroupAggregateUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 调用示例:整个文件复制进 IDE,右键 Run 就能看效果。
*
* <p>分组聚合不依赖外部服务,示例用内存里的订单列表跑通全部入口;
* 日志用 Lombok 的 {@code @Slf4j} + SLF4J 占位符,没装 Lombok 就把 {@code log} 换成项目自己的 Logger。
*/
@Slf4j
public class GroupAggregateUtilsDemo {
/** 示例用的订单,实际换成你自己的实体类 */
public record Order(String shop, String sku, double amount) {
}
/** 六个订单:望京店三笔、国贸店两笔、西单店一笔,末笔与首笔同店同 SKU */
private static final List<Order> ORDERS = List.of(
new Order("望京店", "A1", 38.0),
new Order("国贸店", "A2", 60.0),
new Order("望京店", "B7", 12.5),
new Order("西单店", "A1", 99.0),
new Order("国贸店", "C3", 20.0),
new Order("望京店", "A1", 7.5));
public static void main(String[] args) {
// ========== 一、分组与计数 ==========
Map<String, List<Order>> grouped = GroupAggregateUtils.groupBy(ORDERS, Order::shop);
log.info("「GroupAggregateUtils 方法1 groupBy(collection, keyExtractor)」返回分组Map -> {}", grouped);
Map<String, List<Order>> nullFriendly = GroupAggregateUtils.groupBy(
Arrays.asList(new Order(null, "X1", 5.0), null, new Order("望京店", "X2", 8.0)),
order -> order == null ? "本条无门店" : order.shop());
log.info("「GroupAggregateUtils 方法1 groupBy(collection, keyExtractor)」null 键与 null 元素都能成组 -> {}",
nullFriendly);
Map<String, Long> counts = GroupAggregateUtils.countBy(ORDERS, Order::shop);
log.info("「GroupAggregateUtils 方法2 countBy(collection, keyExtractor)」返回计数Map -> {}", counts);
// ========== 二、数值聚合 ==========
Map<String, Double> sums = GroupAggregateUtils.sumBy(ORDERS, Order::shop, Order::amount);
log.info("「GroupAggregateUtils 方法3 sumBy(collection, keyExtractor, mapper)」返回求和Map -> {}", sums);
Map<String, Double> averages = GroupAggregateUtils.averageBy(ORDERS, Order::shop, Order::amount);
log.info("「GroupAggregateUtils 方法4 averageBy(collection, keyExtractor, mapper)」返回均值Map -> {}",
averages);
// ========== 三、分组最值与取单值 ==========
Comparator<Order> byAmount = Comparator.comparingDouble(Order::amount);
Map<String, Order> highest = GroupAggregateUtils.maxBy(ORDERS, Order::shop, byAmount);
log.info("「GroupAggregateUtils 方法5 maxBy(collection, keyExtractor, comparator)」返回每组最大一单 -> {}",
highest);
Map<String, Order> lowest = GroupAggregateUtils.minBy(ORDERS, Order::shop, byAmount);
log.info("「GroupAggregateUtils 方法6 minBy(collection, keyExtractor, comparator)」返回每组最小一单 -> {}",
lowest);
Map<String, Order> lastOrderOfShop = GroupAggregateUtils.toMap(ORDERS, Order::shop);
log.info("「GroupAggregateUtils 方法7 toMap(collection, keyExtractor)」返回key到单值Map(重复键留后出现的)-> {}",
lastOrderOfShop);
// ========== 四、榜单与安全取组 ==========
List<String> ranked = GroupAggregateUtils.sortKeysByCountDesc(counts);
log.info("「GroupAggregateUtils 方法8 sortKeysByCountDesc(countMap)」返回按笔数降序的门店榜 -> {}", ranked);
List<Order> wangjing = GroupAggregateUtils.getGroup(grouped, "望京店");
log.info("「GroupAggregateUtils 方法9 getGroup(groupedMap, key)」返回该组不可变视图 -> {}", wangjing);
List<Order> notOpened = GroupAggregateUtils.getGroup(grouped, "未开业店");
log.info("「GroupAggregateUtils 方法9 getGroup(groupedMap, key)」key 不存在时 -> {}", notOpened);
// ========== 五、catch异常信息 ==========
// 1. 集合入参为 null
try {
GroupAggregateUtils.groupBy(null, Order::shop);
} catch (IllegalArgumentException e) {
log.error("「异常信息获取演示」集合入参为null -> {}", e.getMessage());
}
// 2. 数值提取函数为 null,消息直接点名是哪个入参
try {
GroupAggregateUtils.sumBy(ORDERS, Order::shop, null);
} catch (IllegalArgumentException e) {
log.error("「异常信息获取演示」数值提取函数为null -> {}", e.getMessage());
}
// 3. 计数 Map 里混入 null 计数值,按契约抛 NullPointerException
try {
Map<String, Long> brokenCounts = new LinkedHashMap<>();
brokenCounts.put("望京店", 3L);
brokenCounts.put("国贸店", null);
GroupAggregateUtils.sortKeysByCountDesc(brokenCounts);
} catch (NullPointerException e) {
log.error("「异常信息获取演示」计数值含null -> 异常类型={}", e.getClass().getSimpleName());
}
// 4. 集合里有 null 元素、key 提取器又没兜底时,提取器的异常不会被工具吞掉
try {
List<Order> dirty = new ArrayList<>(ORDERS);
dirty.add(null);
GroupAggregateUtils.groupBy(dirty, Order::shop);
} catch (NullPointerException e) {
log.error("「异常信息获取演示」null元素撞上不兜底的提取器 -> 异常类型={}", e.getClass().getSimpleName());
}
}
}
五、工具类源码(可直接复制)
📦 「点击此处可下载 Java企业级工具类全家桶-48-分组聚合工具类」 的完整源码,拿来即用。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯