Java 8 Optional 系列 API 分析与 Spring Boot Test 下的业务场景应用

165 阅读7分钟

Java 8 Optional 系列 API 分析与 Spring Boot Test 下的业务场景应用

引言

Java 8 引入的 Optional 类是 Java 编程中处理空值的一种优雅方式,旨在减少 NullPointerException 并提升代码可读性。Optional 提供了一系列 API,允许开发者以函数式编程的方式安全地处理可能为空的对象。本文将详细分析 Optional 的所有 API,结合 Spring Boot Test 和 JUnit 5 的测试场景,基于实际业务场景展示每种 API 的用法,并总结日常开发中需要注意的事项。


一、Java 8 Optional API 全解析

Optional 是一个容器类,可以包含一个非空值或空值。以下是 Optional 类的所有主要 API 方法及其功能:

  1. 创建 Optional 实例

    • Optional.of(T value):创建一个包含非空值的 Optional。如果传入 null,抛出 NullPointerException
    • Optional.ofNullable(T value):如果值非空,创建包含该值的 Optional;如果值为空,创建空的 Optional
    • Optional.empty():创建一个空的 Optional 实例。
  2. 检查值是否存在

    • boolean isPresent():如果 Optional 包含值,返回 true,否则返回 false
    • boolean isEmpty()(Java 11+):如果 Optional 为空,返回 true,否则返回 false
  3. 处理值

    • T get():如果 Optional 包含值,返回该值;否则抛出 NoSuchElementException
    • T orElse(T other):如果值存在,返回该值;否则返回指定的默认值 other
    • T orElseGet(Supplier<? extends T> supplier):如果值存在,返回该值;否则返回 supplier 提供的默认值。
    • T orElseThrow()(Java 10+):如果值存在,返回该值;否则抛出 NoSuchElementException
    • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果值存在,返回该值;否则抛出 exceptionSupplier 提供的异常。
  4. 函数式操作

    • Optional<T> filter(Predicate<? super T> predicate):如果值存在且满足 predicate,返回包含该值的 Optional;否则返回空 Optional
    • <U> Optional<U> map(Function<? super T, ? extends U> mapper):如果值存在,应用 mapper 函数并返回包含结果的 Optional;否则返回空 Optional
    • <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper):如果值存在,应用 mapper 函数(返回 Optional),并返回其结果;否则返回空 Optional
  5. 条件执行

    • void ifPresent(Consumer<? super T> action):如果值存在,执行指定的 action
    • void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)(Java 9+):如果值存在,执行 action;否则执行 emptyAction
    • Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)(Java 9+):如果值存在,返回当前 Optional;否则返回 supplier 提供的 Optional

二、业务场景设计

假设我们开发了一个基于 Spring Boot 的电商系统,包含一个 OrderService,用于处理订单相关逻辑。订单实体 Order 包含以下字段:

  • id:订单 ID
  • userId:用户 ID
  • amount:订单金额
  • status:订单状态(例如 "PENDING", "COMPLETED")

我们需要编写测试类 OrderServiceTest,使用 Spring Boot Test 和 JUnit 5,测试 OrderService 中与 Optional 相关的逻辑。以下是业务场景:

  1. 根据订单 ID 查询订单,可能返回空订单。
  2. 根据用户 ID 查询订单列表,可能返回空列表。
  3. 计算订单折扣,可能因金额不足而无折扣。
  4. 更新订单状态,可能因订单不存在而失败。

三、Spring Boot Test 环境搭建

我们首先搭建一个 Spring Boot 测试环境,整合 JUnit 5 和 Assertions。

1. 项目依赖

pom.xml 中添加必要的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2. Order 实体类

public class Order {
    private Long id;
    private Long userId;
    private BigDecimal amount;
    private String status;

    // 构造函数、Getter 和 Setter
    public Order(Long id, Long userId, BigDecimal amount, String status) {
        this.id = id;
        this.userId = userId;
        this.amount = amount;
        this.status = status;
    }

    public Long getId() { return id; }
    public Long getUserId() { return userId; }
    public BigDecimal getAmount() { return amount; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}

3. OrderService 类

@Service
public class OrderService {
    private Map<Long, Order> orderRepository = new HashMap<>();

    public Optional<Order> findOrderById(Long id) {
        return Optional.ofNullable(orderRepository.get(id));
    }

    public Optional<List<Order>> findOrdersByUserId(Long userId) {
        List<Order> orders = orderRepository.values().stream()
                .filter(order -> userId.equals(order.getUserId()))
                .collect(Collectors.toList());
        return Optional.ofNullable(orders.isEmpty() ? null : orders);
    }

    public Optional<BigDecimal> calculateDiscount(Long orderId) {
        return findOrderById(orderId)
                .filter(order -> order.getAmount().compareTo(BigDecimal.valueOf(100)) > 0)
                .map(order -> order.getAmount().multiply(BigDecimal.valueOf(0.1)));
    }

    public Optional<Order> updateOrderStatus(Long orderId, String newStatus) {
        return findOrderById(orderId)
                .map(order -> {
                    order.setStatus(newStatus);
                    return order;
                });
    }

    // 用于测试的模拟数据
    public void addOrder(Order order) {
        orderRepository.put(order.getId(), order);
    }
}

四、基于 JUnit 5 的 Optional 用法测试

以下是 OrderServiceTest 类,针对每个 Optional API 提供详细测试用例,并结合业务场景解释用法和注意事项。

@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    private Order sampleOrder;

    @BeforeEach
    void setUp() {
        sampleOrder = new Order(1L, 100L, BigDecimal.valueOf(200), "PENDING");
        orderService.addOrder(sampleOrder);
    }

    @Test
    @DisplayName("测试 Optional.of 和 Optional.ofNullable")
    void testCreationMethods() {
        // 使用 Optional.of 创建(值非空)
        Optional<Order> orderOptional = Optional.of(sampleOrder);
        Assertions.assertTrue(orderOptional.isPresent());
        Assertions.assertEquals(sampleOrder, orderOptional.get());

        // 使用 Optional.ofNullable 创建(值可能为空)
        Optional<Order> nullableOptional = Optional.ofNullable(null);
        Assertions.assertFalse(nullableOptional.isPresent());

        // 注意事项:Optional.of(null) 会抛出 NullPointerException
        Assertions.assertThrows(NullPointerException.class, () -> Optional.of(null));
    }

    @Test
    @DisplayName("测试 Optional.empty")
    void testEmpty() {
        Optional<Order> emptyOptional = Optional.empty();
        Assertions.assertFalse(emptyOptional.isPresent());
        Assertions.assertThrows(NoSuchElementException.class, emptyOptional::get);

        // 业务场景:查询不存在的订单
        Optional<Order> nonExistentOrder = orderService.findOrderById(999L);
        Assertions.assertEquals(Optional.empty(), nonExistentOrder);
    }

    @Test
    @DisplayName("测试 isPresent 和 isEmpty")
    void testPresenceChecks() {
        Optional<Order> orderOptional = orderService.findOrderById(1L);
        Assertions.assertTrue(orderOptional.isPresent());
        Assertions.assertFalse(orderOptional.isEmpty()); // Java 11+

        Optional<Order> emptyOptional = orderService.findOrderById(999L);
        Assertions.assertFalse(emptyOptional.isPresent());
        Assertions.assertTrue(emptyOptional.isEmpty()); // Java 11+
    }

    @Test
    @DisplayName("测试 get 方法")
    void testGet() {
        Optional<Order> orderOptional = orderService.findOrderById(1L);
        Assertions.assertEquals(sampleOrder, orderOptional.get());

        // 注意事项:对空 Optional 调用 get 会抛出异常
        Optional<Order> emptyOptional = orderService.findOrderById(999L);
        Assertions.assertThrows(NoSuchElementException.class, emptyOptional::get);
    }

    @Test
    @DisplayName("测试 orElse 和 orElseGet")
    void testOrElseMethods() {
        // orElse:返回默认值
        Order defaultOrder = new Order(0L, 0L, BigDecimal.ZERO, "DEFAULT");
        Optional<Order> nonExistentOrder = orderService.findOrderById(999L);
        Order result = nonExistentOrder.orElse(defaultOrder);
        Assertions.assertEquals(defaultOrder, result);

        // orElseGet:动态生成默认值
        Order generatedOrder = nonExistentOrder.orElseGet(() -> new Order(0L, 0L, BigDecimal.ZERO, "GENERATED"));
        Assertions.assertEquals("GENERATED", generatedOrder.getStatus());

        // 注意事项:orElseGet 延迟执行,适合昂贵的默认值生成
    }

    @Test
    @DisplayName("测试 orElseThrow")
    void testOrElseThrow() {
        Optional<Order> orderOptional = orderService.findOrderById(1L);
        Order result = orderOptional.orElseThrow(() -> new IllegalArgumentException("Order not found"));
        Assertions.assertEquals(sampleOrder, result);

        Optional<Order> emptyOptional = orderService.findOrderById(999L);
        Assertions.assertThrows(IllegalArgumentException.class, () ->
                emptyOptional.orElseThrow(() -> new IllegalArgumentException("Order not found")));
    }

    @Test
    @DisplayName("测试 filter")
    void testFilter() {
        // 业务场景:筛选金额大于 100 的订单
        Optional<Order> orderOptional = orderService.findOrderById(1L)
                .filter(order -> order.getAmount().compareTo(BigDecimal.valueOf(100)) > 0);
        Assertions.assertTrue(orderOptional.isPresent());

        // 测试金额不足的订单
        Order lowAmountOrder = new Order(2L, 100L, BigDecimal.valueOf(50), "PENDING");
        orderService.addOrder(lowAmountOrder);
        Optional<Order> filteredOptional = orderService.findOrderById(2L)
                .filter(order -> order.getAmount().compareTo(BigDecimal.valueOf(100)) > 0);
        Assertions.assertFalse(filteredOptional.isPresent());
    }

    @Test
    @DisplayName("测试 map")
    void testMap() {
        // 业务场景:获取订单金额
        Optional<BigDecimal> amountOptional = orderService.findOrderById(1L)
                .map(Order::getAmount);
        Assertions.assertEquals(BigDecimal.valueOf(200), amountOptional.get());

        // 测试空 Optional
        Optional<BigDecimal> emptyAmount = orderService.findOrderById(999L)
                .map(Order::getAmount);
        Assertions.assertFalse(emptyAmount.isPresent());
    }

    @Test
    @DisplayName("测试 flatMap")
    void testFlatMap() {
        //  bune场景:将订单转换为嵌套的 Optional(例如获取用户的订单列表)
        Optional<List<Order>> ordersOptional = orderService.findOrdersByUserId(100L);
        Optional<Long> orderCount = ordersOptional.flatMap(orders -> Optional.of((long) orders.size()));
        Assertions.assertEquals(1L, orderCount.get());

        // 测试空 Optional
        Optional<List<Order>> emptyOrders = orderService.findOrdersByUserId(999L);
        Optional<Long> emptyCount = emptyOrders.flatMap(orders -> Optional.of((long) orders.size()));
        Assertions.assertFalse(emptyCount.isPresent());
    }

    @Test
    @DisplayName("测试 ifPresent")
    void testIfPresent() {
        // 业务场景:如果订单存在,打印订单状态
        orderService.findOrderById(1L).ifPresent(order ->
                System.out.println("Order status: " + order.getStatus()));
        // 输出:Order status: PENDING

        // 测试空 Optional(不会执行)
        orderService.findOrderById(999L).ifPresent(order ->
                System.out.println("This will not be printed"));
    }

    @Test
    @DisplayName("测试 ifPresentOrElse")
    void testIfPresentOrElse() {
        // 业务场景:订单存在时打印状态,否则打印错误
        orderService.findOrderById(1L).ifPresentOrElse(
                order -> System.out.println("Order status: " + order.getStatus()),
                () -> System.out.println("Order not found"));
        // 输出:Order status: PENDING

        orderService.findOrderById(999L).ifPresentOrElse(
                order -> System.out.println("This will not be printed"),
                () -> System.out.println("Order not found"));
        // 输出:Order not found
    }

    @Test
    @DisplayName("测试 or")
    void testOr() {
        // 业务场景:如果订单不存在,返回默认订单的 Optional
        Optional<Order> defaultOptional = Optional.of(new Order(0L, 0L, BigDecimal.ZERO, "DEFAULT"));
        Optional<Order> result = orderService.findOrderById(999L)
                .or(() -> defaultOptional);
        Assertions.assertEquals("DEFAULT", result.get().getStatus());
    }
}

五、日常工作中使用 Optional 的注意事项

  1. 避免直接使用 get()

    • 调用 get() 前必须检查 isPresent(),否则可能抛出 NoSuchElementException。推荐使用 orElseorElseGetorElseThrow
  2. 优先使用 ofNullable 而非 of

    • Optional.of(null) 会抛出 NullPointerException,而 Optional.ofNullable(null) 安全返回空 Optional
  3. 合理选择 orElse 和 orElseGet

    • orElse 适合简单的默认值,但默认值对象会始终创建。
    • orElseGet 适合动态生成默认值,延迟执行可提高性能。
  4. 避免嵌套 Optional

    • 使用 flatMap 而非 map 来处理返回 Optional 的函数,避免 Optional<Optional<T>>
  5. 不要将 Optional 用于方法参数

    • Optional 设计为返回值类型,用于表示可能为空的结果。方法参数使用 Optional 会增加复杂性。
  6. 结合 Stream API

    • Optional 可以与 Stream API 结合,例如 stream().findFirst() 返回 Optional,提高代码流畅性。
  7. 异常处理

    • 使用 orElseThrow 自定义异常,便于业务场景下的错误处理。
  8. 性能考虑

    • Optional 引入了对象封装,频繁创建可能影响性能。仅在需要显式处理空值时使用。
  9. 测试覆盖

    • 在测试中,确保覆盖 Optional 的空值和非空值场景,避免遗漏边界条件。

六、总结

Optional 是 Java 8 提供的一项强大工具,通过其丰富的 API,我们可以优雅地处理空值问题。在 Spring Boot 测试场景中,结合 JUnit 5 的 Assertions,我们可以全面验证 Optional 的行为。本文通过电商订单的业务场景,展示了 Optional 每个 API 的用法,并总结了实际开发中的注意事项。希望开发者能够灵活运用 Optional,编写更健壮、可读性更高的代码。