主题
界面
主题
Optional 用一个类型回答"这个方法可能返回空":Optional<User> 比 User 多了一层明确的语义——调用方会知道"这里可能没有值",而不是拿到 null 才发现。
本节讲清它的正确用法,以及一个更重要的问题:什么时候不该用它。这个类被误用的比例相当高。
完成本节后,你应该能够:
Optional 要解决的问题(null 的语义缺失);of / ofNullable / empty 正确创建;map / filter / flatMap 写链式转换,替代多层判空;orElse 与 orElseGet 的关键区别;ifPresentOrElse 处理"存在与不存在"两种情况;Optional 的场景,并知道替代写法。null 有三个问题:
第一,它不表达语义。 findUser("u404") 返回 null,是"用户不存在"?还是"查询失败"?调用方不知道。
第二,它容易漏判。 少写一次判空就是一次 NPE,而编译器不会提醒你。
第三,判空代码会层层嵌套。
// 传统写法:每多一层就多一个 if
User user = findUser(id);
if (user != null) {
String email = user.getEmail();
if (email != null) {
return email.toUpperCase();
}
}
return "无邮箱";// Optional 写法:链路式表达
return findUser(id)
.map(User::email)
.map(String::toUpperCase)
.orElse("无邮箱");第二种写法不只是短,而是每一步的"可能为空"都被显式表达出来了。
Optional<User> user = findUser("u1"); // 要么装着 User,要么是空的容器它最多装一个值。"可能没有值"这件事从方法签名上就看得见——这是它最大的价值。
Optional.of("值"); // 值必须非 null,否则抛 NullPointerException
Optional.empty(); // 明确的空
Optional.ofNullable(value); // value 可能是 null(最常用)选择标准:
| 场景 | 用法 |
|---|---|
| 确定值非 null(空值属于编程错误) | Optional.of(value) —— 快速失败 |
值可能为 null(如 map.get(key)) | Optional.ofNullable(value) |
| 明确返回空 | Optional.empty() |
optional.get() // 不推荐:为空时抛 NoSuchElementException
optional.orElse("默认值") // 为空时用默认值
optional.orElseGet(() -> "懒加载默认值") // 为空时才计算默认值
optional.orElseThrow(() -> new IllegalStateException("必须存在"))orElse 与 orElseGet 的关键区别:
// orElse:参数"总会被求值",无论 Optional 是否为空
String value = optional.orElse(loadFromDatabase()); // 数据库查询总是执行!
// orElseGet:只有为空时才执行 Supplier
String value = optional.orElseGet(() -> loadFromDatabase()); // 需要时才查判断标准:默认值的计算有代价(查库、调接口、创建大对象)→ 必须用 orElseGet;只是一个字面量或常量 → orElse 更简洁。
get() 为什么被"不推荐":它把 Optional 变回了"可能抛异常的值",等于绕了一圈又回到手动判空,而且异常是 NoSuchElementException(比 NPE 更陌生)。
findUser(id).map(User::email) // 转换:值存在时应用函数
findUser(id).map(User::email).filter(e -> e.contains("@")) // 过滤:不满足则变空
findUser(id).flatMap(this::findEmailDetail) // 展平:函数本身返回 Optionalmap 与 flatMap 的区别:
// map:函数返回普通值 → 自动包装成 Optional
optional.map(user -> user.name()) // Optional<String>
// flatMap:函数返回 Optional → 避免嵌套两层 Optional<Optional<String>>
optional.flatMap(user -> findEmail(user.id())) // Optional<String>如果对"返回 Optional 的函数"用 map,你会得到 Optional<Optional<String>>——这是 flatMap 存在的理由。
optional.ifPresent(user -> System.out.println(user.name())); // 存在时执行
optional.ifPresentOrElse(
user -> System.out.println("找到: " + user.name()),
() -> System.out.println("未找到")); // Java 9+:两种情况都处理ifPresentOrElse 是 if-else 的表达式版本,避免了 isPresent() + get() 的写法。
List<String> ids = List.of("u1", "u404", "u2");
List<String> names = ids.stream()
.map(this::findUser) // Stream<Optional<User>>
.flatMap(Optional::stream) // Java 9+:把 Optional 展平成流,自动跳过空
.map(User::name)
.toList();Optional::stream 是"批量查询时跳过空值"的标准写法——没有它就要写 filter(Optional::isPresent).map(Optional::get)。
| 场景 | 为什么不该用 | 替代 |
|---|---|---|
| 字段 | 增加序列化复杂度(JPA/Jackson 处理麻烦);字段为 null 本身可以接受 | 直接用对象,或返回 Optional 的 getter |
| 方法参数 | 调用方要多写一层包装;参数可空可以用重载表达 | 重载方法,或明确要求非 null |
| 集合元素 | List<Optional<T>> 没有意义(不该往里放空值) | 直接不放这个元素 |
| 用于基本类型 | Optional<Integer> 有装箱开销 | OptionalInt / OptionalLong / OptionalDouble |
一句话原则:Optional 是给"返回值"用的——它解决的问题是"调用方拿到 null 不知道该怎么办"。其他位置用它,通常是把简单问题复杂化。
Optional<String> present = Optional.of("值");
Optional<String> empty = Optional.empty();
present.orElse("默认值"); // "值"
empty.orElse("默认值"); // "默认值"static Optional<User> findUser(String id) {
return Optional.ofNullable(DATABASE.get(id));
}
// 查询邮箱,找不到给默认值
String email = findUser(id).map(User::email).orElse("无邮箱");
// 查询并校验
boolean valid = findUser(id)
.map(User::email)
.filter(e -> e.contains("@"))
.isPresent();// 反例一:isPresent + get(等于手动判空)
if (optional.isPresent()) {
return optional.get().name(); // 与 if (obj != null) 相比没有改进
}
// 反例二:Optional 做字段/参数(见 5.7 节)
// 反例三:orElse 里放昂贵操作(每次调用都执行)
String name = optional.orElse(loadFromDatabase()); // 无论是否为空都会查库文件:labs/functional/src/main/java/cn/kb/learn/functional/OptionalLesson.java
package cn.kb.learn.functional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class OptionalLesson {
record User(String name, String email) {
}
private static final Map<String, User> DATABASE = Map.of(
"u1", new User("张三", "zhangsan@test.com"));
/** 查询可能找不到:返回 Optional 而不是 null */
static Optional<User> findUser(String id) {
return Optional.ofNullable(DATABASE.get(id));
}
public static void main(String[] args) {
// 1. 三种创建方式
Optional<String> present = Optional.of("值");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null);
System.out.println("of: " + present + ",empty: " + empty + ",ofNullable(null): " + nullable);
try {
Optional.of(null);
} catch (NullPointerException exception) {
System.out.println("Optional.of(null) 抛 NullPointerException");
}
// 2. 取值:orElse / orElseGet / orElseThrow
System.out.println("orElse: " + empty.orElse("默认值"));
System.out.println("orElseGet: " + empty.orElseGet(() -> "懒加载默认值"));
try {
empty.orElseThrow(() -> new IllegalStateException("值必须存在"));
} catch (IllegalStateException exception) {
System.out.println("orElseThrow: " + exception.getMessage());
}
// 3. 存在时执行
findUser("u1").ifPresent(user -> System.out.println("ifPresent 找到: " + user.name()));
findUser("u404").ifPresentOrElse(
user -> System.out.println("ifPresentOrElse 找到: " + user.name()),
() -> System.out.println("ifPresentOrElse 未找到,走 else 分支"));
// 4. 链式转换:避免层层判空
System.out.println("查询 u1 的邮箱: " + findUser("u1").map(User::email).orElse("无邮箱"));
System.out.println("查询 u404 的邮箱: " + findUser("u404").map(User::email).orElse("无邮箱"));
// 5. filter:条件不满足时返回空 Optional
System.out.println("邮箱以 zhangsan 开头: "
+ findUser("u1").map(User::email).filter(email -> email.startsWith("zhangsan")).isPresent());
System.out.println("邮箱以 other 开头: "
+ findUser("u1").map(User::email).filter(email -> email.startsWith("other")).orElse("被过滤掉了"));
// 6. flatMap:避免 Optional 嵌套
Function<User, Optional<String>> emailLookup = user -> Optional.ofNullable(user.email())
.filter(email -> email.contains("@"));
System.out.println("flatMap 链式查询: " + findUser("u1").flatMap(emailLookup).orElse("无"));
System.out.println("对不存在的用户 flatMap: " + findUser("u404").flatMap(emailLookup).orElse("无"));
// 7. 反例:isPresent + get(等于没解决空指针问题)
if (findUser("u1").isPresent()) {
System.out.println("不推荐的写法(isPresent + get): " + findUser("u1").get().name());
}
// 8. 与 Stream 配合:Optional::stream 自动跳过空值
List<String> ids = List.of("u1", "u404", "u2");
List<String> names = ids.stream()
.map(OptionalLesson::findUser)
.flatMap(Optional::stream)
.map(User::name)
.toList();
System.out.println("批量查询(自动跳过不存在的): " + names);
}
}运行命令:
cd labs
./mvnw -pl functional compile
java -cp functional/target/classes cn.kb.learn.functional.OptionalLesson预期输出:
of: Optional[值],empty: Optional.empty,ofNullable(null): Optional.empty
Optional.of(null) 抛 NullPointerException
orElse: 默认值
orElseGet: 懒加载默认值
orElseThrow: 值必须存在
ifPresent 找到: 张三
ifPresentOrElse 未找到,走 else 分支
查询 u1 的邮箱: zhangsan@test.com
查询 u404 的邮箱: 无邮箱
邮箱以 zhangsan 开头: true
邮箱以 other 开头: 被过滤掉了
flatMap 链式查询: zhangsan@test.com
对不存在的用户 flatMap: 无
不推荐的写法(isPresent + get): 张三
批量查询(自动跳过不存在的): [张三]三处值得对照:
| 输出 | 含义 |
|---|---|
ofNullable(null): Optional.empty | 把可能为 null 的值安全地包装起来 |
查询 u404 的邮箱: 无邮箱 | 链式调用中任何一步为空,整体就走默认分支,不会 NPE |
批量查询(自动跳过不存在的): [张三] | flatMap(Optional::stream) 自动过滤掉了不存在的那两个 id |
findUser("u404").map(User::email) 处断点,观察 map 收到空 Optional 时直接返回空,不执行函数——这就是"不会 NPE"的原因;ifPresentOrElse 的两个 lambda 里各打一个断点,分别用 u1 与 u404 触发,看走了哪个分支;orElseGet(() -> "懒加载默认值") 改成 orElse(compute("默认值")),并在 compute 方法里打断点——你会发现即使 Optional 有值,它也会执行;Optional 对象,能看到内部的 value 字段(空时为 null)——它就是一个简单的容器。get() 之前不判断 Optional<User> user = findUser(id);
user.get().name(); // 为空时抛 NoSuchElementException修复:用 map + orElse / orElseThrow / ifPresent。get() 在规范的项目里应该被视为禁用方法。
orElse 里放昂贵计算 String value = optional.orElse(loadFromDatabase()); // 总是执行
String value = optional.orElseGet(() -> loadFromDatabase()); // 需要时才执行class User {
private Optional<String> nickname; // 反模式
}
void update(Optional<String> nickname) { } // 反模式:调用方要写 Optional.ofNullable(...)修复:字段直接用可空对象;参数用重载或明确非空。
Optional<Optional<String>> nested = optional.map(this::findEmail); // 嵌套
Optional<String> flat = optional.flatMap(this::findEmail); // 正确// 不必要:值已经确定非空
String name = Optional.of(user.getName()).orElse("默认"); // 直接 user.getName() 就行
// 不必要:判空逻辑本来很简单
if (list != null && !list.isEmpty()) { } // 比 Optional 包装更清楚判断标准:用 Optional 应该是为了让代码更清楚。如果它让代码更绕,就是误用。
目标:把一段多层判空的代码改写成 Optional 链。
步骤:
Optional 链重写它(map + orElse),对比代码量;filter:只在城市不为空且长度大于 1 时返回;Optional<Order> findLatestOrder(List<Order> orders):返回金额最大的订单,列表为空时返回 Optional.empty();orElseThrow 与 orElse 各调用一次第 5 步的方法,说明两种选择的适用场景。验收方式:能说出第 6 步两种选择的区别("调用方能否在缺失时给出合理默认")。
扩展任务:对比 Optional<List<String>> 与 List<String>(空列表)两种表达"没有数据"的方式,说明各自适合什么场景。
基础题
Optional.get() 在为空时会返回 null。(对 / 错)Optional 用作字段类型?代码阅读题
record Config(String value) {}
Map<String, String> map = new HashMap<>();
map.put("timeout", "30");
Optional<String> timeout = Optional.ofNullable(map.get("timeout"));
System.out.println(timeout.map(String::toUpperCase).orElse("默认"));
Optional<String> missing = Optional.ofNullable(map.get("missing"));
System.out.println(missing.map(String::toUpperCase).orElse("默认"));编程题
Optional<String> get(String key):从内部 Map 读配置;int getInt(String key, int defaultValue):读到就转成 int,读不到或格式错误就用默认值(不能用 try-catch 处理"读不到",但可以用它处理"格式错误");String getRequired(String key):读不到就抛 IllegalStateException;main 演示三种情况。综合任务
UserRepository(Optional<User> findById(String))、AddressRepository(Optional<Address> findByUserId(String));String describeCity(String userId):返回用户所在城市,用户或地址缺失时分别返回不同的提示(如"用户不存在"/"地址未填写");if (x != null),全部用 Optional 链;提示 1(方向):第 4 题的关键是 map 在空 Optional 上不会执行函数。
提示 2(关键点):第 5 题的 getInt 用 orElse(defaultValue) 处理"读不到",用 try-catch 包住 Integer.parseInt 处理"格式错误"——两者是不同的问题。
提示 3(接近答案):第 6 题区分"用户不存在"与"地址未填写"需要分别在两步用 orElse:
User user = userRepository.findById(userId).orElse(null);
if (user == null) return "用户不存在";
return addressRepository.findByUserId(userId).map(Address::city).orElse("地址未填写");——注意这里 orElse(null) 又回到了 null,说明"需要区分多个缺失原因"时 Optional 链不够用,更好的做法是返回更丰富的结果类型(如 Result 或抛不同异常)。
get() 在为空时抛 NoSuchElementException,不是返回 null。这也说明它为什么该被避免:它把"空"变成了异常,而不是让调用方优雅处理。Optional.ofNullable(value);orElseGet(() -> ...)。Optional 字段需要额外配置;② 字段为 null 本身是可接受的(对象刚创建时字段可能未赋值);③ 它会让"读写字段"多一层包装,收益却是零。Optional 的设计意图是"返回值"。30
默认timeout 有值,map(String::toUpperCase) 把 "30" 转换后返回,orElse 不触发;missing 是空 Optional,map 直接返回空而不执行函数(所以不会对 null 调用 toUpperCase),orElse 返回 "默认"。关键机制:map 内部会先判断是否有值,为空时直接返回 Optional.empty()。这就是 Optional 链"天然防 NPE"的原因。
package cn.kb.learn.functional;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class ConfigReader {
private final Map<String, String> values = new HashMap<>();
public ConfigReader put(String key, String value) {
values.put(key, value);
return this;
}
public Optional<String> get(String key) {
return Optional.ofNullable(values.get(key));
}
public int getInt(String key, int defaultValue) {
return get(key)
.map(text -> {
try {
return Integer.parseInt(text.trim());
} catch (NumberFormatException exception) {
return defaultValue;
}
})
.orElse(defaultValue);
}
public String getRequired(String key) {
return get(key).orElseThrow(() -> new IllegalStateException("缺少必需配置: " + key));
}
public static void main(String[] args) {
ConfigReader config = new ConfigReader()
.put("timeout", "30")
.put("retries", "abc");
System.out.println("timeout: " + config.getInt("timeout", 10));
System.out.println("retries(格式错误,用默认值): " + config.getInt("retries", 3));
System.out.println("missing(不存在,用默认值): " + config.getInt("missing", 5));
System.out.println("timeout 字符串: " + config.get("timeout").orElse("未配置"));
try {
config.getRequired("database.url");
} catch (IllegalStateException exception) {
System.out.println("预期内的失败: " + exception.getMessage());
}
}
}注意 getInt 的分工:Optional 负责"键是否存在",try-catch 负责"值格式是否正确"——两件不同的事,用两种不同的机制,不要把格式错误也塞给 Optional。
预期输出:
timeout: 30
retries(格式错误,用默认值): 3
missing(不存在,用默认值): 5
timeout 字符串: 30
预期内的失败: 缺少必需配置: database.urlpackage cn.kb.learn.functional;
import java.util.Optional;
public class UserCityService {
record User(String id, String name) {
}
record Address(String userId, String city) {
}
interface UserRepository {
Optional<User> findById(String id);
}
interface AddressRepository {
Optional<Address> findByUserId(String userId);
}
private final UserRepository userRepository;
private final AddressRepository addressRepository;
UserCityService(UserRepository userRepository, AddressRepository addressRepository) {
this.userRepository = userRepository;
this.addressRepository = addressRepository;
}
/** 纯 Optional 链写法(无法区分"用户不存在"与"地址未填") */
String describeCitySimple(String userId) {
return userRepository.findById(userId)
.flatMap(user -> addressRepository.findByUserId(user.id()))
.map(Address::city)
.orElse("未知");
}
/** 需要区分缺失原因时:牺牲一点"纯净",换取准确的提示 */
String describeCity(String userId) {
Optional<User> user = userRepository.findById(userId);
if (user.isEmpty()) {
return "用户不存在";
}
return addressRepository.findByUserId(user.get().id())
.map(Address::city)
.filter(city -> !city.isBlank())
.orElse("地址未填写");
}
public static void main(String[] args) {
UserRepository users = id -> "u1".equals(id)
? Optional.of(new User("u1", "张三"))
: Optional.empty();
AddressRepository addresses = userId -> "u1".equals(userId)
? Optional.empty()
: Optional.empty();
UserCityService service = new UserCityService(users, addresses);
System.out.println("纯链式(丢失原因): " + service.describeCitySimple("u404"));
System.out.println("区分原因 - 用户不存在: " + service.describeCity("u404"));
System.out.println("区分原因 - 地址未填: " + service.describeCity("u1"));
}
}关于"Optional 够不够用"的结论:
Optional 只表达"有 / 没有",不表达"为什么没有"。所以:
| 需求 | 合适方案 |
|---|---|
| 缺失时用默认值 | Optional + orElse / orElseGet |
| 缺失时需要区分原因 | 抛不同的异常(《异常体系与处理》)、或返回自定义 Result 类型 |
| 缺失是可接受的结果 | 就是 Optional 的最佳场景 |
这正是"工具要匹配问题"的体现:Optional 不是万能的,硬用它表达"多个失败原因",代码会比直接判空更绕。
预期输出:
纯链式(丢失原因): 未知
区分原因 - 用户不存在: 用户不存在
区分原因 - 地址未填: 地址未填写ofNullable 包装可能为空的值;map / filter / flatMap 写链式转换;orElse 与 orElseGet 的区别并按场景选择;Optional::stream 在批量查询中的用法;Optional 的场景;Optional 与流的配合S1 只剩最后三节:《线程与同步基础》、《Java 8 核心特性总览》、《Java 9-21 新特性与 Record》。
暂无