第 15 章 / 共 20 章
Optional 与防空
用 Optional 表达"可能没有"、链式取值、避免反模式
null 的问题与 Optional 出现的原因
Java 长期被 NullPointerException 折磨:返回值是 null 还是真实对象?调用方往往要靠文档(甚至运行时崩溃)才能知道。Java 8 引入 Optional<T>——一种"可能有值、也可能没有"的容器类型,在 API 签名上明确表达"这个返回值需要你处理 null 情况"。
- ✅ 最佳应用场景:作为方法返回值——"按 id 查不到用户"、"集合是空时取第一个"、"Map 查不到 key"
- ✅ 适合:用作 Stream.findFirst / Map.get 等"可能没有"的语义
- ❌ 反模式:用作字段(占内存又难序列化)、用作方法参数(应该用方法重载)、用作集合元素
- 原则:Optional 表达的是"接口契约"——告诉调用者"你必须明确处理空情况"
创建 Optional
// CreateOptional.java
import java.util.Optional;
public class CreateOptional {
public static void main(String[] args) {
Optional<String> a = Optional.of("alice"); // 值不能是 null,否则 NPE
Optional<String> b = Optional.ofNullable(null); // 允许 null,得到 empty
Optional<String> c = Optional.empty(); // 显式空
System.out.println(a.isPresent()); // true
System.out.println(b.isPresent()); // false
System.out.println(c.isEmpty()); // true (Java 11+)
}
}取值:orElse / orElseGet / orElseThrow
三种取值方式的关键区别在"何时计算 fallback":orElse 总会先算好 fallback;orElseGet 只在 empty 时调 supplier(延迟计算,开销大的 fallback 推荐用它);orElseThrow 没值则抛异常。
// GetValue.java
import java.util.Optional;
public class GetValue {
static String expensiveDefault() {
System.out.println("computing default...");
return "default";
}
public static void main(String[] args) {
Optional<String> has = Optional.of("hello");
Optional<String> none = Optional.empty();
// orElse:永远先算 expensiveDefault(),哪怕 Optional 有值
System.out.println(has.orElse(expensiveDefault())); // 先打印 "computing...",再 hello
// orElseGet:只在 empty 时调用 supplier
System.out.println(has.orElseGet(GetValue::expensiveDefault)); // 不会打印 "computing..."
System.out.println(none.orElseGet(GetValue::expensiveDefault)); // 会
// orElseThrow:用于"应该有值但没有"的异常路径
try {
none.orElseThrow(() -> new IllegalStateException("user not found"));
} catch (IllegalStateException e) {
System.out.println("caught: " + e.getMessage());
}
}
}map / flatMap 链式取值
应用场景:用户对象 -> 地址对象 -> 城市名,任意一层可能 null。传统 null 检查写成嵌套 if 又长又脆弱;Optional 链 + map/flatMap 一行搞定。
// ChainNav.java
import java.util.Optional;
public class ChainNav {
record Address(String city) {}
record User(String name, Address address) {}
record Order(User buyer) {}
static Optional<Order> lookup(long id) {
return id == 1
? Optional.of(new Order(new User("Alice", new Address("Shanghai"))))
: Optional.empty();
}
public static void main(String[] args) {
// 传统嵌套 null 检查(反例)
Order order = lookup(1).orElse(null);
String city = null;
if (order != null && order.buyer() != null && order.buyer().address() != null) {
city = order.buyer().address().city();
}
System.out.println("old: " + city);
// 链式:清晰、无 NPE 风险
String city2 = lookup(1)
.map(Order::buyer)
.map(User::address)
.map(Address::city)
.orElse("<unknown>");
System.out.println("new: " + city2);
// 不存在的 order
String city3 = lookup(999)
.map(Order::buyer)
.map(User::address)
.map(Address::city)
.orElse("<unknown>");
System.out.println("none: " + city3);
}
}map vs flatMap:当映射函数本身返回 Optional 时用 flatMap,否则用 map。否则会出现 Optional<Optional<...>>。
// FlatMap.java
import java.util.Optional;
public class FlatMap {
static Optional<String> nickname(String name) {
return name.equals("Alice") ? Optional.of("Al") : Optional.empty();
}
public static void main(String[] args) {
Optional<String> user = Optional.of("Alice");
// map 把 Optional<String> 包装成 Optional<Optional<String>>,不对
// Optional<Optional<String>> wrong = user.map(FlatMap::nickname);
// flatMap 自动拆掉一层
Optional<String> nick = user.flatMap(FlatMap::nickname);
System.out.println(nick.orElse("-"));
}
}ifPresent / ifPresentOrElse
副作用风格的处理:有值时做一件事、可能还要在空时做另一件事。比 if (opt.isPresent()) 更声明式。
// IfPresent.java
import java.util.Optional;
public class IfPresent {
public static void main(String[] args) {
Optional<String> name = Optional.of("Alice");
name.ifPresent(n -> System.out.println("hello, " + n));
Optional<String> none = Optional.empty();
none.ifPresentOrElse(
n -> System.out.println("hello, " + n),
() -> System.out.println("no user")
);
}
}filter:条件成立才保留
// FilterOpt.java
import java.util.Optional;
public class FilterOpt {
public static void main(String[] args) {
Optional<String> name = Optional.of("");
String result = name
.filter(s -> !s.isBlank()) // 空字符串被过滤掉
.orElse("<empty>");
System.out.println(result); // <empty>
}
}搭配 Stream
应用场景:把 Stream 里的 Optional 结果拍平成实际值的流;或者 findFirst 之后再做下一步。
// StreamOpt.java
import java.util.List;
import java.util.Optional;
public class StreamOpt {
static Optional<Integer> parsePositive(String s) {
try {
int v = Integer.parseInt(s);
return v > 0 ? Optional.of(v) : Optional.empty();
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static void main(String[] args) {
List<String> inputs = List.of("3", "-1", "abc", "7", "0");
// 解析每个、去掉无效的、求和。Optional::stream 是 Java 9+
int sum = inputs.stream()
.map(StreamOpt::parsePositive)
.flatMap(Optional::stream)
.mapToInt(Integer::intValue)
.sum();
System.out.println(sum); // 10
}
}实战:Repository 接口的典型用法
Spring Data 的 JpaRepository.findById 就返回 Optional<T>。模仿这个签名能让 API 显著更清晰——调用方必然要处理"找不到"的情况。
// RepoExample.java
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class RepoExample {
record User(long id, String name, String email) {}
static class UserRepository {
private final Map<Long, User> db = new HashMap<>();
UserRepository() {
db.put(1L, new User(1, "Alice", "alice@ex.com"));
db.put(2L, new User(2, "Bob", "bob@ex.com"));
}
Optional<User> findById(long id) {
return Optional.ofNullable(db.get(id));
}
}
public static void main(String[] args) {
UserRepository repo = new UserRepository();
// 1) 取邮箱,不存在用默认
String email = repo.findById(1)
.map(User::email)
.orElse("unknown");
System.out.println(email);
// 2) 不存在则抛业务异常
User u = repo.findById(99)
.orElseThrow(() -> new IllegalStateException("user 99 not found"));
System.out.println(u); // 不会执行,前一行抛了
}
}反模式
- 不要把 Optional 作为类的字段:占内存、难序列化,没人这么用
- 不要把 Optional 作为方法参数:用方法重载更清晰
- 不要返回 Optional<List<T>>:返回空列表 List.of() 即可
- 不要返回 Optional<T[]>:用空数组
- 不要 opt.get() 不判空:和 throw NullPointerException 没区别
- 不要 opt.isPresent() + opt.get():用 ifPresent / map / orElse