Skip to content

第34课:Optional

🎯 学习目标

  • 理解 Optional 的目的(显式表达可能为空,避免 NPE)
  • 掌握创建(of/ofNullable/empty)、取值(orElse/orElseGet/orElseThrow)
  • 掌握链式操作(map/flatMap/filter/ifPresent)
  • 知道 Optional 的正确用法与陷阱(不该用于字段/参数/集合)

📖 一、概念讲解:为什么需要 Optional

1. NPE 的痛点

Java 最常见的运行时异常是 NullPointerException。根源:null 是"隐式"的——方法返回 String,可能返回 null,调用者不知,直接 .toUpperCase() 就 NPE。

java
String name = user.getName();   // 可能 null
name.toUpperCase();              // NPE!

防御式编程需要到处判空(if (name != null)),代码冗长、易漏。

2. Optional 是什么

Optional<T> 是一个容器对象,要么持有非 null 的 T,要么为空(empty)。它把"可能为空"显式化到类型签名里:

java
Optional<String> getName() { ... }   // 明确告诉调用者:可能没值,必须处理

调用者拿到 Optional 必须主动解包,逼着处理"空"的情况,减少忘记判空导致的 NPE。

3. Optional 不是什么

  • 不是 null 的完全替代——它只在方法返回值表达"可能空"。
  • 不是 为了消除所有 NPE——仍能用 optional.get() 不检查就取(抛异常)。
  • 不推荐用作字段、方法参数、集合元素(见陷阱)。

📖 二、创建

java
Optional<String> o1 = Optional.of("hello");          // 非 null 值,传 null 抛 NPE
Optional<String> o2 = Optional.ofNullable(null);     // 允许 null,返回 empty
Optional<String> o3 = Optional.empty();              // 空的

of vs ofNullable

  • of(value):值确定非 null 时用,传 null 立即暴露问题(fail-fast)。
  • ofNullable(value):值可能 null 时用,null 转 empty。

📖 三、判断与取值

java
Optional<String> opt = Optional.ofNullable(getName());

// 判断
opt.isPresent();      // 有值?
opt.isEmpty();        // 空?(JDK 11+)

// 取值
String v1 = opt.get();                 // 不安全!空时抛 NoSuchElementException
String v2 = opt.orElse("默认");         // 空时给默认值
String v3 = opt.orElseGet(() -> computeDefault());  // 空时调 Supplier 计算
String v4 = opt.orElseThrow(() -> new MyException());  // 空时抛指定异常

get() 是危险操作——空时抛异常,和直接用 null 没本质区别。优先用 orElse 系列。

orElse vs orElseGet

  • orElse(默认值):默认值总是计算(即使有值也计算,若默认值是常量无妨,若是方法调用则浪费)。
  • orElseGet(Supplier):仅空时才调用 Supplier 计算。默认值昂贵时用它延迟计算。

📖 四、链式操作

Optional 支持流式链式,避免层层判空:

java
// 传统层层判空
if (user != null) {
    Address addr = user.getAddress();
    if (addr != null) {
        City city = addr.getCity();
        if (city != null) System.out.println(city.getName());
    }
}

// Optional 链式
Optional.ofNullable(user)
    .map(User::getAddress)       // User → Optional<Address>
    .map(Address::getCity)       // Optional<Address> → Optional<City>
    .map(City::getName)          // Optional<City> → Optional<String>
    .ifPresent(System.out::println);   // 有值才打印

任一环节为空,整个链返回 empty,ifPresent 不执行——优雅避免 NPE。

map vs flatMap

  • map(fn):fn 返回普通值,自动包装成 Optional。
  • flatMap(fn):fn 返回 Optional,扁平化避免 Optional&lt;Optional&lt;T&gt;&gt;(与 Stream/CompletableFuture 同理)。

若 getter 本身返回 Optional,用 flatMap:

java
opt.flatMap(User::getAddressOpt)   // getAddressOpt 返回 Optional<Address>

filter

java
opt.filter(s -> s.length() > 3)   // 不满足条件的变 empty

ifPresent / ifPresentOrElse

java
opt.ifPresent(v -> use(v));                          // 有值执行
opt.ifPresentOrElse(v -> use(v), () -> handleEmpty()); // 有值/空分别处理(JDK 9+)

⚠️ 五、常见陷阱与误用

陷阱1:用 get() 不检查

java
String s = opt.get();   // 空时抛异常,等于没解决 NPE

修复:用 orElse/orElseGet/ifPresent/isPresent 判断后再 get。

陷阱2:用 Optional 作字段

java
class User { Optional<String> name; }   // ❌ 不推荐

Optional 不可序列化、占内存、字段本该用 null 表达空。Optional 用于方法返回值。

陷阱3:用 Optional 作方法参数

java
void foo(Optional<String> name) { }   // ❌ 不推荐

参数为空用重载或 @Nullable 更清晰。Optional 参数增加调用方负担(要包 Optional.of)。

陷阱4:用 Optional 作集合元素

java
List<Optional<String>> list;   // ❌ 空集合或 null 元素即可

陷阱5:orElse 里放昂贵调用

java
opt.orElse(expensiveCompute());   // 总是计算 expensiveCompute,即使 opt 有值

修复opt.orElseGet(() -&gt; expensiveCompute()),仅空时计算。

陷阱6:把 isPresent+get 当 if-null 用

java
if (opt.isPresent()) use(opt.get()); else default;
// 等价于 if (x != null),没发挥 Optional 优势

修复:用 orElse/ifPresent/map 链式。


🆚 六、Java vs 其他语言

特性CJava OptionalKotlin/Swift
null 处理手动判空Optional 容器可空类型系统(?)
编译期保证无(运行时)有(编译期强制)

对 C 程序员:Optional 是"显式表示可能为空"的容器,比 C 的"到处 if (p != NULL)"更类型安全。但比 Kotlin/Swift 的可空类型弱(后者编译期强制,Java Optional 仍能被绕过)。Optional 是工具,用好减少 NPE,用错(到处 isPresent+get)等于没解决。


💡 七、最佳实践

  1. 用于方法返回值,表达"可能空",逼调用者处理。
  2. 优先 orElse/orElseGet/ifPresent/map,避免 get。
  3. 昂贵默认值用 orElseGet,避免 orElse 浪费计算。
  4. 链式访问替代层层判空:map/flatMap。
  5. 不用作字段/参数/集合元素
  6. 别为 Optional 而 Optional:确定非空直接返回值,别包 Optional。

📝 练习预告

完成 练习/Ex34_Optional.java 中的 6 道题:

  1. 创建与取值(of/ofNullable/orElse)
  2. isPresent + ifPresent
  3. map 链式转换
  4. flatMap 扁平化
  5. filter 过滤
  6. 综合:嵌套对象的 Optional 链式访问

完成后对比 答案/Sol34.java,查看逐行讲解与多解法。


📖 八、Optional 的设计边界

Optional&lt;T&gt; 的核心目标是让方法签名显式表达“可能没有值”。

推荐:

java
public Optional<User> findById(Long id) {
    return userRepository.findById(id);
}

不推荐:

java
private Optional<String> name;

public void setName(Optional<String> name) {
    this.name = name;
}

原因:

text
Optional 不是字段容器。
序列化框架处理 Optional 字段可能不自然。
方法参数用 Optional 会让调用方更啰嗦。
集合元素用 Optional 会增加理解成本。

字段、参数和集合中仍然使用普通类型,边界处做好校验。


📖 九、orElse 与 orElseGet

两者差异非常重要:

java
String value = optional.orElse(expensiveDefault());

expensiveDefault() 无论 Optional 是否有值都会执行。

java
String value = optional.orElseGet(() -> expensiveDefault());

orElseGet 只有在 Optional 为空时才执行。

如果默认值创建成本高,必须使用 orElseGet


🧪 十、实战案例:嵌套对象取值

传统写法:

java
String city = null;
if (user != null && user.getAddress() != null && user.getAddress().getCity() != null) {
    city = user.getAddress().getCity();
}

Optional 写法:

java
String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("未知城市");

这类链式访问是 Optional 最适合的场景之一。


📌 十一、与异常的关系

找不到数据不一定是异常。

java
public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}

但业务上必须存在时,可以在边界转换为异常:

java
User user = userRepository.findByEmail(email)
    .orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "用户不存在"));

Optional 表达“不一定有”,异常表达“业务不允许没有”。


🔍 十二、自测问题

text
Optional.of 和 Optional.ofNullable 有什么区别?
为什么不推荐 Optional.get?
orElse 和 orElseGet 的执行时机有什么区别?
Optional 适合作为字段吗?
Optional 和异常分别表达什么语义?
map 和 flatMap 在 Optional 中有什么区别?
为什么 Optional 不能彻底消灭 NullPointerException?
Spring Data JPA 为什么常返回 Optional<T>?

🧭 十三、Optional 决策清单

决定是否使用 Optional 时,可以按下面规则判断:

text
方法是否可能没有返回值?
调用方是否必须显式处理“没有”?
“没有”是否是正常业务结果,而不是异常?
返回类型是否是单个对象,而不是集合?
是否处在领域边界、Repository 查询或配置读取位置?

适合返回 Optional:

java
Optional<User> findUserById(long id);
Optional<String> getConfig(String key);
Optional<Discount> calculateDiscount(Order order);

不适合返回 Optional:

java
List<User> listUsers();          // 空集合即可
void update(Optional<User> user); // 参数不推荐
class User { Optional<String> name; } // 字段不推荐

判断口诀:

text
返回单个可能缺失的值:可以 Optional。
返回多个值:用空集合。
调用方传入值:用重载、校验或普通 null 约定。
业务必须存在:用异常表达。

🧪 十四、实战案例:查询到业务响应

Repository 层:

java
public Optional<User> findById(long id) {
    User user = database.queryUser(id);
    return Optional.ofNullable(user);
}

Service 层把“可能没有”转换为业务语义:

java
public UserProfile getProfile(long userId) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new BusinessException("USER_NOT_FOUND", "用户不存在"));

    return new UserProfile(
        user.getId(),
        Optional.ofNullable(user.getNickname()).orElse("未设置昵称")
    );
}

Controller 层不应该继续暴露 Optional:

java
public ApiResponse<UserProfile> getProfile(long userId) {
    return ApiResponse.ok(userService.getProfile(userId));
}

分层原则:

text
Repository 可以返回 Optional,表达查询缺失。
Service 决定缺失是异常、默认值还是空响应。
Controller 返回明确 DTO,不把 Optional 泄漏给前端协议。

🛠 十五、空值治理策略

Optional 只是空值治理的一部分,不是万能解。

更完整的策略:

text
入口参数做校验,尽早拒绝非法 null。
内部领域模型保持不变量,减少可空字段。
查询缺失用 Optional 表达。
业务必须存在用异常表达。
集合查询返回空集合。
DTO 输出阶段把 null 约定清楚。
团队统一 @Nullable / @NonNull 注解规范。

常见团队约定:

text
Service 公共方法参数默认不允许 null。
Repository 单对象查询返回 Optional<T>。
Repository 列表查询返回 List<T>,不返回 null。
DTO 字段是否允许 null 在接口文档中说明。
Optional 不进入 JSON 响应模型。

✅ 十六、掌握标准

学完本课后,应能做到:

text
能解释 Optional 主要用于方法返回值。
能区分 Optional.empty、null 和异常的语义。
能正确选择 of、ofNullable、empty。
能避免 get、isPresent + get 的伪改进写法。
能解释 orElse 与 orElseGet 的性能差异。
能用 map、flatMap、filter 完成链式处理。
能在 Repository、Service、Controller 分层中合理使用 Optional。
能制定简单的团队空值治理约定。

如果项目里 Optional 到处出现在字段、参数、集合元素中,通常不是“更安全”,而是空值边界没有设计清楚。


🎓 下一步

  • 第35课:JVM内存模型 — 堆、栈、方法区、内存溢出(JVM 系列开始)