当前位置:首页 > 文章列表 > 文章 > 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
  • 终端能运行 javacjava 命令。
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推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    2511次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    2317次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    2266次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    2463次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    2442次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码