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 方法及其功能:
-
创建 Optional 实例
Optional.of(T value):创建一个包含非空值的Optional。如果传入null,抛出NullPointerException。Optional.ofNullable(T value):如果值非空,创建包含该值的Optional;如果值为空,创建空的Optional。Optional.empty():创建一个空的Optional实例。
-
检查值是否存在
boolean isPresent():如果Optional包含值,返回true,否则返回false。boolean isEmpty()(Java 11+):如果Optional为空,返回true,否则返回false。
-
处理值
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提供的异常。
-
函数式操作
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。
-
条件执行
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:订单 IDuserId:用户 IDamount:订单金额status:订单状态(例如 "PENDING", "COMPLETED")
我们需要编写测试类 OrderServiceTest,使用 Spring Boot Test 和 JUnit 5,测试 OrderService 中与 Optional 相关的逻辑。以下是业务场景:
- 根据订单 ID 查询订单,可能返回空订单。
- 根据用户 ID 查询订单列表,可能返回空列表。
- 计算订单折扣,可能因金额不足而无折扣。
- 更新订单状态,可能因订单不存在而失败。
三、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 的注意事项
-
避免直接使用 get()
- 调用
get()前必须检查isPresent(),否则可能抛出NoSuchElementException。推荐使用orElse、orElseGet或orElseThrow。
- 调用
-
优先使用 ofNullable 而非 of
Optional.of(null)会抛出NullPointerException,而Optional.ofNullable(null)安全返回空Optional。
-
合理选择 orElse 和 orElseGet
orElse适合简单的默认值,但默认值对象会始终创建。orElseGet适合动态生成默认值,延迟执行可提高性能。
-
避免嵌套 Optional
- 使用
flatMap而非map来处理返回Optional的函数,避免Optional<Optional<T>>。
- 使用
-
不要将 Optional 用于方法参数
Optional设计为返回值类型,用于表示可能为空的结果。方法参数使用Optional会增加复杂性。
-
结合 Stream API
Optional可以与 Stream API 结合,例如stream().findFirst()返回Optional,提高代码流畅性。
-
异常处理
- 使用
orElseThrow自定义异常,便于业务场景下的错误处理。
- 使用
-
性能考虑
Optional引入了对象封装,频繁创建可能影响性能。仅在需要显式处理空值时使用。
-
测试覆盖
- 在测试中,确保覆盖
Optional的空值和非空值场景,避免遗漏边界条件。
- 在测试中,确保覆盖
六、总结
Optional 是 Java 8 提供的一项强大工具,通过其丰富的 API,我们可以优雅地处理空值问题。在 Spring Boot 测试场景中,结合 JUnit 5 的 Assertions,我们可以全面验证 Optional 的行为。本文通过电商订单的业务场景,展示了 Optional 每个 API 的用法,并总结了实际开发中的注意事项。希望开发者能够灵活运用 Optional,编写更健壮、可读性更高的代码。