0

0

Java Stream API处理嵌套对象字段求和:以购物车条目为例

聖光之護

聖光之護

发布时间:2025-08-31 12:51:17

|

151人浏览过

|

来源于php中文网

原创

Java Stream API处理嵌套对象字段求和:以购物车条目为例

本文深入探讨如何利用Java Stream API高效地对复杂嵌套对象中的数值字段进行聚合求和。通过一个购物车条目的具体案例,演示了如何从多层级对象中提取并累加BigDecimal类型的费用数据,强调了mapToDouble、BigDecimal的精确计算以及在处理可能为空的字段时,结合注解进行null值判断的关键注意事项。

在现代java应用开发中,数据模型往往涉及多层嵌套的对象结构。当我们需要从这些深层结构中提取特定数值并进行聚合计算时,如何高效且优雅地实现是一个常见挑战。本教程将以一个典型的电商购物车场景为例,展示如何使用java stream api来解决这一问题。

问题场景:聚合嵌套费用数据

假设我们有以下一系列Java类,它们共同构成了购物车中的一个条目及其相关的费用信息:

  • CartEntry:购物车条目,包含一个Fulfillment对象,并计划存储totalDeliveryFee。
  • Fulfillment:配送信息,包含一个StoreDelivery对象。
  • StoreDelivery:门店配送详情,包含cost(成本)和deliveryServiceFee(配送服务费),以及一个DeliveryWindow对象。
  • DeliveryWindow:配送窗口,包含一个fee(窗口费)。

我们的目标是从一个CartEntry列表中,将每个CartEntry中嵌套的cost、deliveryServiceFee和fee这三个BigDecimal类型的字段值累加起来,形成一个总的配送费用。

传统迭代与Stream API的对比

如果采用传统的循环迭代方式,我们需要编写多层嵌套的if判断和for循环来逐层访问对象并累加,代码会显得冗长且易出错。Java 8引入的Stream API为这种场景提供了更简洁、更具表达力的解决方案。

采用Stream API进行高效求和

解决此问题的核心在于利用Stream API的mapToDouble操作,结合BigDecimal的精确计算能力。以下是实现这一目标的推荐方法:

立即学习Java免费学习笔记(深入)”;

琅琅配音
琅琅配音

全能AI配音神器

下载
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

// 假设的类结构,与问题描述一致
class CartEntry {
    private Fulfillment fulfillment;
    private BigDecimal totalDeliveryFee = BigDecimal.ZERO; // 初始化为0

    public CartEntry(Fulfillment fulfillment) {
        this.fulfillment = fulfillment;
    }

    public Fulfillment getFulfillment() {
        return fulfillment;
    }

    public BigDecimal getTotalDeliveryFee() {
        return totalDeliveryFee;
    }

    public void setTotalDeliveryFee(BigDecimal totalDeliveryFee) {
        this.totalDeliveryFee = totalDeliveryFee;
    }
}

class Fulfillment {
    private StoreDelivery storeDelivery;

    public Fulfillment(StoreDelivery storeDelivery) {
        this.storeDelivery = storeDelivery;
    }

    public StoreDelivery getStoreDelivery() {
        return storeDelivery;
    }
}

class StoreDelivery {
    private BigDecimal cost = BigDecimal.ZERO;
    private BigDecimal deliveryServiceFee = BigDecimal.ZERO;
    private DeliveryWindow deliveryWindow;

    public StoreDelivery(BigDecimal cost, BigDecimal deliveryServiceFee, DeliveryWindow deliveryWindow) {
        this.cost = cost;
        this.deliveryServiceFee = deliveryServiceFee;
        this.deliveryWindow = deliveryWindow;
    }

    public BigDecimal getCost() {
        return cost;
    }

    public BigDecimal getDeliveryServiceFee() {
        return deliveryServiceFee;
    }

    public DeliveryWindow getDeliveryWindow() {
        return deliveryWindow;
    }
}

class DeliveryWindow {
    private BigDecimal fee = BigDecimal.ZERO;

    public DeliveryWindow(BigDecimal fee) {
        this.fee = fee;
    }

    public BigDecimal getFee() {
        return fee;
    }
}

public class CartEntryTotalDeliveryFeeCalculator {

    public static double calculateTotalDeliveryFee(List entries) {
        if (entries == null || entries.isEmpty()) {
            return 0.0;
        }

        return entries.stream()
            .mapToDouble(entry -> {
                // 初始化当前条目的总费用
                BigDecimal currentEntryTotal = BigDecimal.ZERO;

                // 尝试获取CartEntry自身的totalDeliveryFee(如果已有值)
                currentEntryTotal = currentEntryTotal.add(
                    Optional.ofNullable(entry.getTotalDeliveryFee()).orElse(BigDecimal.ZERO)
                );

                // 尝试获取Fulfillment -> StoreDelivery -> cost
                currentEntryTotal = currentEntryTotal.add(
                    Optional.ofNullable(entry.getFulfillment())
                        .map(Fulfillment::getStoreDelivery)
                        .map(StoreDelivery::getCost)
                        .orElse(BigDecimal.ZERO)
                );

                // 尝试获取Fulfillment -> StoreDelivery -> deliveryServiceFee
                currentEntryTotal = currentEntryTotal.add(
                    Optional.ofNullable(entry.getFulfillment())
                        .map(Fulfillment::getStoreDelivery)
                        .map(StoreDelivery::getDeliveryServiceFee)
                        .orElse(BigDecimal.ZERO)
                );

                // 尝试获取Fulfillment -> StoreDelivery -> DeliveryWindow -> fee
                currentEntryTotal = currentEntryTotal.add(
                    Optional.ofNullable(entry.getFulfillment())
                        .map(Fulfillment::getStoreDelivery)
                        .map(StoreDelivery::getDeliveryWindow)
                        .map(DeliveryWindow::getFee)
                        .orElse(BigDecimal.ZERO)
                );

                return currentEntryTotal.doubleValue();
            })
            .sum();
    }

    public static void main(String[] args) {
        // 示例数据
        DeliveryWindow dw1 = new DeliveryWindow(new BigDecimal("5.00"));
        StoreDelivery sd1 = new StoreDelivery(new BigDecimal("10.50"), new BigDecimal("2.25"), dw1);
        Fulfillment f1 = new Fulfillment(sd1);
        CartEntry ce1 = new CartEntry(f1);
        ce1.setTotalDeliveryFee(new BigDecimal("1.00")); // 假设CartEntry自身可能也有一个初始费用

        DeliveryWindow dw2 = new DeliveryWindow(new BigDecimal("3.00"));
        StoreDelivery sd2 = new StoreDelivery(new BigDecimal("7.00"), new BigDecimal("1.50"), dw2);
        Fulfillment f2 = new Fulfillment(sd2);
        CartEntry ce2 = new CartEntry(f2);

        // 包含null值的场景
        CartEntry ce3 = new CartEntry(null); // 没有Fulfillment
        CartEntry ce4 = new CartEntry(new Fulfillment(null)); // 没有StoreDelivery
        CartEntry ce5 = new CartEntry(new Fulfillment(new StoreDelivery(new BigDecimal("2.00"), null, null))); // 部分字段为null

        List entries = List.of(ce1, ce2, ce3, ce4, ce5);

        double total = calculateTotalDeliveryFee(entries);
        System.out.println("所有购物车条目的总配送费用: " + total); // 期望值: (1.00 + 10.50 + 2.25 + 5.00) + (0 + 7.00 + 1.50 + 3.00) + (0) + (0) + (2.00) = 32.25
    }
}

代码解析

  1. entries.stream(): 创建一个CartEntry对象的流。
  2. mapToDouble(entry -> { ... }): 这是核心部分。对于流中的每个CartEntry对象,我们执行一个映射操作,将其转换为一个double值。
    • 在映射函数内部,我们初始化一个BigDecimal变量currentEntryTotal来累积当前CartEntry的所有相关费用。
    • 链式访问与Optional处理: 为了安全地访问深层嵌套的对象字段并避免NullPointerException,我们使用了Optional.ofNullable()。
      • Optional.ofNullable(entry.getFulfillment()):如果fulfillment不为null,则返回一个包含它的Optional;否则返回Optional.empty()。
      • .map(Fulfillment::getStoreDelivery):如果上一步的Optional有值,则继续映射到StoreDelivery对象。
      • .map(StoreDelivery::getCost):继续映射到cost字段。
      • .orElse(BigDecimal.ZERO):如果任何一步的映射导致Optional为空(即某个中间对象或最终字段为null),则返回BigDecimal.ZERO作为替代值,确保求和操作不会中断。
    • currentEntryTotal.add(...): 将通过Optional安全获取的BigDecimal值累加到currentEntryTotal中。
    • .doubleValue(): 在返回前,将最终的BigDecimal总和转换为double类型,以供mapToDouble使用。
  3. .sum(): 对mapToDouble生成的所有double值进行最终求和,得到所有购物车条目的总配送费用。

注意事项

  1. BigDecimal的精度: 在处理货币或任何需要精确计算的数值时,始终优先使用BigDecimal而非double或float。doubleValue()方法在最终求和前将BigDecimal转换为double,如果对最终结果的精度有极高要求,可以考虑使用Stream并配合reduce操作来保持BigDecimal的精度,但这样会稍微复杂一些。

    // 保持BigDecimal精度的求和示例
    BigDecimal totalBigDecimal = entries.stream()
        .map(entry -> {
            BigDecimal currentEntryTotal = BigDecimal.ZERO;
            // ... (同上,通过Optional获取并add所有BigDecimal字段)
            return currentEntryTotal;
        })
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    System.out.println("所有购物车条目的总配送费用 (BigDecimal): " + totalBigDecimal);
  2. Null值处理与注解:

    • 在原始问题中,类字段使用了@NotNull和@DecimalMin("0.0")等注解。这些注解通常在编译时或运行时由验证框架(如JSR 303/380 Bean Validation)进行检查,以确保字段不为null且满足最小值。
    • 如果这些注解能保证在业务逻辑执行到此处时,所有BigDecimal字段(如cost, deliveryServiceFee, fee)及其父对象都不会是null,并且BigDecimal类型的字段在未显式赋值时会自动初始化为BigDecimal.ZERO,那么上述代码中的Optional.ofNullable().orElse(BigDecimal.ZERO)部分可以简化,直接通过getter方法访问。
    • 然而,如果这些保证不完全可靠(例如,对象可能在不经过完整验证流程的情况下被创建或修改),或者中间对象(如fulfillment或storeDelivery)可能为null,那么使用Optional进行防御性编程仍然是最佳实践,以增强代码的健壮性。本教程中的示例代码已包含这种防御性处理。
  3. 代码可读性: 当嵌套层级过多时,map操作内部的逻辑可能会变得复杂。可以考虑将获取单个CartEntry总费用的逻辑封装到一个私有辅助方法中,以提高可读性。

总结

通过Java Stream API,我们可以以声明式的方式优雅地处理复杂嵌套对象中的数据聚合任务。结合mapToDouble和BigDecimal的精确计算,以及Optional的防御性编程,能够编写出既高效又健壮的代码。在实际开发中,理解数据模型的约束(如注解带来的null值保证)将有助于我们选择最简洁有效的实现方式。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

866

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

745

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

741

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

420

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

447

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

431

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16947

2023.08.03

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

58

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.5万人学习

Java 教程
Java 教程

共578课时 | 50.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号