Collectors.groupingBy 适用于按字段分组后进一步统计的多维分析场景,需先明确分组键(如 categoryId、shopId、userId),再结合嵌套 groupingBy 或下游收集器实现销量、均价、金额分布等聚合。

用 Collectors.groupingBy 统计电商订单子项(OrderItem)的多维属性,关键不是套用方法,而是理清分组维度、聚合逻辑和嵌套结构。它天然适合“按某字段分组 → 对每组做进一步统计”的场景,比如按商品类目统计销量、按店铺统计平均单价、按用户ID聚合订单金额分布等。
明确分组键:选对主维度是前提
订单子项通常含 skuId、categoryId、shopId、userId、quantity、price 等字段。groupingBy 的第一个参数必须是函数式接口 Function super T, ? extends K>,即从 OrderItem 映射出分组依据。
- 按商品类目汇总:`groupingBy(item -> item.getCategoryId())`
- 按店铺+用户联合分组:`groupingBy(item -> new ShopUserKey(item.getShopId(), item.getUserId()))`(需重写 equals/hashCode)
- 按价格区间分桶:`groupingBy(item -> item.getPrice().compareTo(BigDecimal.valueOf(100))
叠加下游收集器:不止于分组列表
默认 groupingBy 返回 Map<k list>></k>,但实际中更常需要每组的统计值,这时要用下游收集器(downstream collector):
- 统计每类商品总销量:
groupingBy(item -> item.getCategoryId(), summingInt(OrderItem::getQuantity)) - 计算每店铺平均成交单价:
groupingBy(item -> item.getShopId(), averagingDouble(item -> item.getPrice().doubleValue())) - 获取每用户购买的最高单价商品:
groupingBy(item -> item.getUserId(), collectingAndThen(maxBy(Comparator.comparing(OrderItem::getPrice)), Optional::get)) - 按类目分组并同时统计数量、总价、均价:
groupingBy(item -> item.getCategoryId(), summarizingDouble(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).doubleValue()))
多级嵌套分组:处理复合分析需求
当需“先按类目,再按价格带”或“先按店铺,再按下单小时”时,可嵌套 groupingBy:
- 二级分组示例(类目 → 价格区间 → 子项列表):
groupingBy(item -> item.getCategoryId(),<br> groupingBy(item -> item.getPrice().compareTo(BigDecimal.valueOf(50)) < 0 ? "0-50" : "50+")) - 三级统计(店铺 → 类目 → 总销售额):
groupingBy(OrderItem::getShopId,<br> groupingBy(OrderItem::getCategoryId,<br> summingDouble(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).doubleValue())))
注意:嵌套越深,Map 结构越复杂(如 Map<string map double>></string>),建议封装为静态方法或使用 record 类解包。
处理空值与异常:生产环境不可忽略
电商数据常含 null 字段(如未填类目、价格为 null),直接 map 可能抛 NPE:
- 安全取值:`item -> Optional.ofNullable(item.getCategoryId()).orElse("UNKNOWN")`
- 过滤脏数据:先用
filter预处理流,如 `.filter(item -> item.getPrice() != null && item.getQuantity() > 0)` - 自定义分组键对象时,确保 null 字段参与 equals/hashCode 计算(推荐用 Lombok 的 @Data + @NonNull 或手动处理)
不复杂但容易忽略。

















