当前位置:首页 > 文章列表 > 文章 > java教程 > Java Stream 分组统计实验:从订单列表到客户消费汇总

Java Stream 分组统计实验:从订单列表到客户消费汇总

来源:17golang原创 2026-06-27 20:30:39 0浏览 收藏

后台系统里经常会遇到一类需求:拿到一批订单记录,过滤掉退款或取消的订单,再按客户统计订单数和消费金额。这个场景看起来简单,但如果直接在循环里不断改变量,代码很快会变得难读;如果金额用了 double,又容易留下精度问题。

这篇文章用一个小实验演示 Java Stream 的分组统计写法。读完之后,你可以把同样的结构迁移到报表接口、定时汇总任务、导出前的数据聚合等场景里。

目录
  • 前置条件:这次实验要完成什么
  • 初始化:准备订单模型和样例数据
  • 编写代码:过滤、分组和金额求和
  • 运行检查:核对输出是否符合预期
  • 扩展实验:按客户和 SKU 双维度汇总
  • 常见坑和清理总结

前置条件:这次实验要完成什么

实验目标很明确:输入一组订单,保留已支付订单,按 customerId 分组,最后得到每个客户的订单数量和消费金额。为了让结果可信,我们还会写出期望输出,并在运行后逐行核对。

Java Stream 订单分组统计流程图

需要准备的环境很少:

  • JDK 17 或更高版本,方便使用 record 写样例模型。
  • 一个空目录,例如 stream-summary-lab。
  • 终端能运行 javac 和 java 命令。
mkdir stream-summary-lab
cd stream-summary-lab
touch OrderSummaryLab.java

初始化:准备订单模型和样例数据

先把订单状态、订单记录和汇总结果都放在一个文件里。真实项目通常会拆成多个类,这里为了实验清晰,先保持单文件。

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class OrderSummaryLab {
    enum Status {
        PAID, REFUNDED, CANCELED
    }

    record Order(
            long id,
            long customerId,
            String sku,
            BigDecimal amount,
            Status status
    ) {}

    record CustomerSummary(
            long customerId,
            long orderCount,
            BigDecimal totalAmount
    ) {}

    public static void main(String[] args) {
        List orders = List.of(
                new Order(1, 101, "book", new BigDecimal("89.00"), Status.PAID),
                new Order(2, 101, "pen", new BigDecimal("59.50"), Status.PAID),
                new Order(3, 102, "mouse", new BigDecimal("36.00"), Status.PAID),
                new Order(4, 101, "bag", new BigDecimal("120.00"), Status.REFUNDED),
                new Order(5, 103, "desk", new BigDecimal("499.00"), Status.CANCELED)
        );

        Map summaryMap = summarizeByCustomer(orders);
        summaryMap.values().forEach(summary -> System.out.printf(
                "%d -> orders=%d, total=%s%n",
                summary.customerId(),
                summary.orderCount(),
                summary.totalAmount()
        ));
    }

    private static Map summarizeByCustomer(List orders) {
        return orders.stream()
                .filter(order -> order.status() == Status.PAID)
                .collect(Collectors.groupingBy(
                        Order::customerId,
                        TreeMap::new,
                        Collectors.collectingAndThen(
                                Collectors.toList(),
                                OrderSummaryLab::toCustomerSummary
                        )
                ));
    }

    private static CustomerSummary toCustomerSummary(List orders) {
        long customerId = orders.get(0).customerId();
        BigDecimal total = orders.stream()
                .map(Order::amount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        return new CustomerSummary(customerId, orders.size(), total);
    }
}

编写代码:过滤、分组和金额求和

这段代码的核心链路可以拆成四步:先过滤状态,再按客户编号分组,接着把每组订单转成汇总对象,最后用 TreeMap 让输出顺序稳定。

这里有两个细节值得注意。

  • Collectors.groupingBy 的第二个参数传入 TreeMap::new,方便我们在命令行里稳定核对输出。
  • 金额求和使用 BigDecimal,并用 reduce(BigDecimal.ZERO, BigDecimal::add) 聚合,避免浮点误差。

如果你只需要返回总金额,也可以把 toCustomerSummary 再拆小;如果业务还要返回平均客单价、最大订单金额,就可以在这个方法里继续补字段。关键是让分组逻辑和组内汇总逻辑分开,后面维护更直接。

运行检查:核对输出是否符合预期

在当前目录编译并运行:

javac OrderSummaryLab.java
java OrderSummaryLab

预期输出如下:

101 -> orders=2, total=148.50
102 -> orders=1, total=36.00

这个结果说明三件事:

  • 客户 101 的退款订单没有进入统计。
  • 客户 103 的取消订单没有进入统计。
  • 客户 101 的两笔已支付订单金额被正确相加。

如果你的输出顺序不同,先检查是否少写了 TreeMap::new;如果金额出现很多小数位,检查样例里是否误用了 double 构造金额。

扩展实验:按客户和 SKU 双维度汇总

真实报表往往不只按客户统计,还会继续按商品、渠道、城市等维度拆分。我们可以增加一个组合键,再按 customerId + sku 汇总。

Java Stream 双维度汇总检查图

record CustomerSkuKey(long customerId, String sku) {}

record SkuSummary(
        long customerId,
        String sku,
        long orderCount,
        BigDecimal totalAmount
) {}

private static Map summarizeByCustomerAndSku(List orders) {
    return orders.stream()
            .filter(order -> order.status() == Status.PAID)
            .collect(Collectors.groupingBy(
                    order -> new CustomerSkuKey(order.customerId(), order.sku()),
                    Collectors.collectingAndThen(
                            Collectors.toList(),
                            group -> {
                                CustomerSkuKey key = new CustomerSkuKey(
                                        group.get(0).customerId(),
                                        group.get(0).sku()
                                );
                                BigDecimal total = group.stream()
                                        .map(Order::amount)
                                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                                return new SkuSummary(
                                        key.customerId(),
                                        key.sku(),
                                        group.size(),
                                        total
                                );
                            }
                    )
            ));
}

这个扩展实验的价值在于:分组键不一定只能是单个字段。只要组合键的语义清楚,后面的汇总逻辑仍然可以保持稳定。Java 的 record 默认提供按字段比较的能力,很适合作为这种临时统计键。

常见坑和清理总结

最后把容易踩的点列出来,方便你把实验代码迁移到项目里。

  • 金额不要用 double 保存和汇总,优先使用 BigDecimal 或数据库端的定点数类型。
  • 分组前先过滤状态,避免退款、取消、删除等记录混进正常报表。
  • 输出给接口时不要直接暴露原始实体,建议转成面向页面或接口的汇总 DTO。
  • 样例里使用 TreeMap 是为了稳定输出;线上接口如需指定排序,应在返回前显式排序。
  • 分组维度变多时,优先定义清晰的组合键,不要把多个字段拼成一个模糊字符串。

这次实验完成了一个最小但完整的分组统计链路:准备订单数据,过滤已支付记录,按客户分组,求订单数和总金额,再通过运行结果验证逻辑。后续你可以把同样的结构扩展到日报、月报、运营看板或导出任务里。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
PHP 文件上传生命周期:从表单校验到存储和过期清理PHP 文件上传生命周期:从表单校验到存储和过期清理
上一篇
PHP 文件上传生命周期:从表单校验到存储和过期清理
Linux 日志清理配方:用 find、du 和 gzip 控制磁盘占用
下一篇
Linux 日志清理配方:用 find、du 和 gzip 控制磁盘占用
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    256次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    301次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    280次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    257次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    67次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码