🚀 Java 巩固进阶 · 第 27 天

主题:Optional 类 —— 优雅解决空指针异常(NPE)

📅 进度概览:继 Lambda 和 Stream 之后,今天学习 JDK8 另一个重要特性:Optional 类。它是 Java 对"null 值处理"的函数式封装,旨在减少恼人的 NullPointerException

💡 核心价值

  • 代码健壮:强制开发者显式处理空值情况,减少线上 NPE 故障。
  • 语义清晰:方法返回 Optional<T> 明确告知调用者"结果可能为空"。
  • 链式调用:配合 Stream 和 Lambda,实现流畅的空值处理逻辑。
  • 框架集成:Spring Data JPA 的 findById 返回 Optional,必须掌握。

一、为什么需要 Optional?告别"十亿美元错误" 🛡️

1. 传统 null 处理的痛点

// ❌ 传统写法:层层嵌套 if,代码缩进灾难
public String getCity(User user) {
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            City city = address.getCity();
            if (city != null) {
                return city.getName();
            }
        }
    }
    return "Unknown";
}

2. Optional 的核心思想

┌─────────────────────────────────────┐
│  🔄 Optional 本质                    │
│  是一个容器对象,可能包含也可能不包含非空值│
│                                      │
│  • 不是用来替代所有 null 的          │
│  • 主要用于方法返回值                │
│  • 强制调用者处理空值情况            │
└─────────────────────────────────────┘

💡 一句话理解
“Optional 是一个盒子,里面可能有值,也可能空着。你要先检查盒子再拿东西,或者直接告诉我不值钱时给什么替代品”


二、核心 API 实战:创建与获取 🛠️

1. 创建 Optional 的 3 种方式

// 1️⃣ of():值不能为 null,否则抛 NPE
Optional<String> opt1 = Optional.of("Hello"); 
// Optional.of(null); // ❌ 报错!

// 2️⃣ ofNullable():值可以为 null(⭐ 最常用)
String str = null;
Optional<String> opt2 = Optional.ofNullable(str); // ✅ 安全

// 3️⃣ empty():创建一个空的 Optional
Optional<String> opt3 = Optional.empty();

2. 获取值的 4 种方式(⭐ 必背)

Optional<String> opt = Optional.ofNullable(getValue());

// 1️⃣ get():直接获取,为空抛异常(⚠️ 不推荐单独使用)
// String value = opt.get(); 

// 2️⃣ orElse(default):为空则返回默认值
String value1 = opt.orElse("Default Value");

// 3️⃣ orElseGet(supplier):为空则执行 lambda 生成默认值(性能更好)
String value2 = opt.orElseGet(() -> "Generated: " + System.currentTimeMillis());

// 4️⃣ orElseThrow():为空则抛异常
String value3 = opt.orElseThrow(() -> new RuntimeException("Value not found"));

3. 消费值:ifPresent

// ✅ 推荐:有值则执行,无值则跳过
opt.ifPresent(value -> System.out.println("值:" + value));

// ✅ 进阶:有值执行 A,无值执行 B
opt.ifPresentOrElse(
    value -> System.out.println("值:" + value),
    () -> System.out.println("值为空")
);

三、进阶用法:链式调用与嵌套对象 🔗

1. 嵌套对象安全访问(核心场景)

// ✅ Optional 链式调用:替代层层 if
public String getCityName(Optional<User> userOpt) {
    return userOpt
        .map(User::getAddress)      // User -> Optional<Address>
        .map(Address::getCity)      // Address -> Optional<City>
        .map(City::getName)         // City -> Optional<String>
        .orElse("Unknown City");    // 最终默认值
}

⚠️ 关键细节

  • map():返回值包装进 Optional
  • flatMap():返回值已经是 Optional,避免嵌套 Optional<Optional<T>>

2. 过滤与转换

Optional<Integer> result = Optional.ofNullable(getNumber())
    .filter(n -> n > 10)       // 过滤:不大于 10 变空
    .map(n -> n * 2);          // 转换:乘 2

3. 与 Stream 结合

List<String> names = users.stream()
    .map(User::getName)           // 可能返回 null
    .map(Optional::ofNullable)    // 转 Optional
    .flatMap(Optional::stream)    // 过滤掉空的(JDK9+)
    .collect(Collectors.toList());

四、⚠️ 使用禁忌与最佳实践

1. 不要这样做 ❌

// ❌ 1. 不要用 Optional 包装基本类型
Optional<int> opt = Optional.of(1); // 编译报错!
// ✅ 用 OptionalInt, OptionalLong, OptionalDouble

// ❌ 2. 不要把 Optional 用作字段或方法参数
public class User {
    private Optional<String> name; // ❌ 不推荐,增加序列化复杂度
}
public void save(Optional<User> user) {} // ❌ 调用方负担重

// ❌ 3. 不要直接调用 get() 而不检查
String val = opt.get(); // 可能抛 NoSuchElementException

2. 推荐这样做 ✅

// ✅ 1. 仅用于方法返回值
public Optional<User> findById(Long id) { ... }

// ✅ 2. 优先使用 orElse/orElseGet
String name = userOpt.map(User::getName).orElse("Anonymous");

// ✅ 3. 链式调用保持流畅
Optional.ofNullable(order)
    .map(Order::getPayment)
    .ifPresent(payment -> process(payment));

五、🎯 今日实战任务:空值处理重构

任务 1:基础 Optional 使用

/**
 * 要求:
 * 1. 编写方法 getValue(boolean hasValue) 返回 Optional<String>
 * 2. 调用方使用 orElse 获取默认值
 * 3. 调用方使用 ifPresent 打印值
 * 
 * 💡 提示:
 * return hasValue ? Optional.of("Data") : Optional.empty();
 */

任务 2:嵌套对象安全访问

/**
 * 业务场景:获取用户所在城市的邮编
 * 对象链:User -> Address -> City -> ZipCode
 * 
 * 要求:
 * 1. 创建 User, Address, City 类(关联关系)
 * 2. 编写传统写法(多层 if null 检查)
 * 3. 编写 Optional 写法(map 链式调用)
 * 4. 对比代码行数与可读性
 * 
 * 💡 挑战:
 * - 如果中间某层为空,返回 "000000"
 */

任务 3:Optional 与 Stream 结合

/**
 * 业务场景:过滤列表中的空值并转换
 * 
 * 要求:
 * 1. 创建 List<String> 包含 null 值
 * 2. 用 Stream + Optional 过滤掉 null
 * 3. 将所有非空字符串转大写并收集
 * 
 * 💡 关键 API:
 * .filter(Objects::nonNull) 或 .flatMap(Optional::stream)
 */

任务 4:Spring Data JPA 模拟

/**
 * 要求:
 * 1. 模拟 Repository findById 方法,返回 Optional<User>
 * 2. Service 层调用该方法
 * 3. 如果用户不存在,抛 BusinessException
 * 4. 如果存在,返回用户姓名
 * 
 * 💡 提示:
 * userOpt.orElseThrow(() -> new BusinessException("用户不存在"))
 */

📝 第 27 天 · 核心总结(极简背诵版)

  1. 创建方式

    Optional.of(value)        // 值非空
    Optional.ofNullable(value)// 值可空(⭐ 常用)
    Optional.empty()          // 空对象
    
  2. 获取值

    opt.orElse(default)       // 默认值
    opt.orElseGet(lambda)     // 延迟生成默认值
    opt.orElseThrow(ex)       // 空则抛异常
    opt.ifPresent(consumer)   // 有值则消费
    
  3. 链式调用

    opt.map(...).map(...).orElse(...)
    // 注意:返回值是 Optional 时用 flatMap
    
  4. 使用禁忌

    • ❌ 不做字段/参数
    • ❌ 不包装基本类型(用 OptionalInt 等)
    • ❌ 不直接 get()

明天预告:🔧 接口默认方法与静态方法 —— 接口进化论!

  • 为什么接口需要 default 方法?(兼容旧实现类)
  • 默认方法与抽象类方法的冲突解决
  • 接口静态方法的调用方式
  • 实战:定义可演进的计算接口

准备好了吗?明天我们完善接口的"进化能力"! 🧬✨

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐