Appearance
第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<Optional<T>>(与 Stream/CompletableFuture 同理)。
若 getter 本身返回 Optional,用 flatMap:
java
opt.flatMap(User::getAddressOpt) // getAddressOpt 返回 Optional<Address>filter
java
opt.filter(s -> s.length() > 3) // 不满足条件的变 emptyifPresent / 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(() -> expensiveCompute()),仅空时计算。
陷阱6:把 isPresent+get 当 if-null 用
java
if (opt.isPresent()) use(opt.get()); else default;
// 等价于 if (x != null),没发挥 Optional 优势修复:用 orElse/ifPresent/map 链式。
🆚 六、Java vs 其他语言
| 特性 | C | Java Optional | Kotlin/Swift |
|---|---|---|---|
| null 处理 | 手动判空 | Optional 容器 | 可空类型系统(?) |
| 编译期保证 | 无 | 无(运行时) | 有(编译期强制) |
对 C 程序员:Optional 是"显式表示可能为空"的容器,比 C 的"到处 if (p != NULL)"更类型安全。但比 Kotlin/Swift 的可空类型弱(后者编译期强制,Java Optional 仍能被绕过)。Optional 是工具,用好减少 NPE,用错(到处 isPresent+get)等于没解决。
💡 七、最佳实践
- 用于方法返回值,表达"可能空",逼调用者处理。
- 优先 orElse/orElseGet/ifPresent/map,避免 get。
- 昂贵默认值用 orElseGet,避免 orElse 浪费计算。
- 链式访问替代层层判空:map/flatMap。
- 不用作字段/参数/集合元素。
- 别为 Optional 而 Optional:确定非空直接返回值,别包 Optional。
📝 练习预告
完成 练习/Ex34_Optional.java 中的 6 道题:
- 创建与取值(of/ofNullable/orElse)
- isPresent + ifPresent
- map 链式转换
- flatMap 扁平化
- filter 过滤
- 综合:嵌套对象的 Optional 链式访问
完成后对比 答案/Sol34.java,查看逐行讲解与多解法。
📖 八、Optional 的设计边界
Optional<T> 的核心目标是让方法签名显式表达“可能没有值”。
推荐:
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 系列开始)