Java Optional 深度实战——不只是判空,它改变了我的代码风格
Java Optional 深度实战——不只是判空,它改变了我的代码风格
适读人群:了解 Optional 基本用法但没有深入使用的 Java 工程师 | 阅读时长:约13分钟 | 核心价值:让 Optional 真正提升代码质量,而不是换了个形式的 null 检查
很多人用 Optional 的方式是这样的:
// 用 Optional 之前
if (user != null) {
return user.getName();
}
return "unknown";
// 用 Optional 之后(很多人的写法)
Optional<User> optUser = Optional.ofNullable(user);
if (optUser.isPresent()) {
return optUser.get().getName();
}
return "unknown";这样用 Optional,不如不用——代码更长了,但没有任何实质改进。
这篇文章我想说的是,Optional 真正的价值不在于"避免 NPE",而在于强迫你去思考一个值"可能不存在"这件事,并在 API 设计层面把这个可能性显式表达出来。
一、Optional 的设计意图
Optional 本来是用来做方法返回类型的,而不是用来做普通变量的类型。
// Optional 的正确用法:方法的返回类型
public Optional<User> findById(Long id) {
// 可能找到,可能找不到
}
// 不建议的用法:作为参数类型
public void sendEmail(Optional<String> email) { ... } // 不好,直接用 null 或重载
// 不建议的用法:作为类的字段
class UserProfile {
private Optional<String> nickname; // 不好,用 @Nullable 或普通 null
}当方法返回 Optional<User> 时,它在 API 层面就告诉调用者:"这个值可能不存在,你必须处理这种情况。" 这是一种设计契约,比 @Nullable 注解更有强制力(因为编译器会提醒你)。
二、不要用 isPresent() + get()
这是最常见的反模式:
Optional<User> optUser = userRepository.findById(id);
if (optUser.isPresent()) {
User user = optUser.get();
// ... 处理 user
}这和原来的 if (user != null) 在语义上没有任何区别,只是换了个外壳。
Optional 的价值在于它的那些方法:
map 和 flatMap:链式转换
// 从用户的部门里拿部门名称(用户可能不存在,部门可能不存在)
Optional<String> deptName = userRepository.findById(id)
.map(User::getDepartment) // User -> Department,结果是 Optional<Department>
.map(Department::getName); // Department -> String,结果是 Optional<String>
// 如果 getDepartment() 本身返回 Optional<Department>,用 flatMap
Optional<String> deptName = userRepository.findById(id)
.flatMap(User::getDepartment) // 避免 Optional<Optional<Department>>
.map(Department::getName);orElse / orElseGet / orElseThrow:处理不存在的情况
// orElse:不存在时返回默认值(注意:即使存在,defaultValue 也会被求值)
String name = optUser.map(User::getName).orElse("anonymous");
// orElseGet:不存在时调用 supplier(懒求值,推荐)
String name = optUser.map(User::getName).orElseGet(() -> getDefaultName());
// orElseThrow:不存在时抛异常(Java 10+,不带参数默认抛 NoSuchElementException)
User user = optUser.orElseThrow(() -> new UserNotFoundException(id));orElse 和 orElseGet 的区别很重要:
// orElse 总是求值 defaultValue,即使 Optional 有值
String result = Optional.of("hello")
.orElse(expensiveCompute()); // expensiveCompute() 总是会被调用!
// orElseGet 是懒求值,只有 Optional 为空时才调用 supplier
String result = Optional.of("hello")
.orElseGet(() -> expensiveCompute()); // expensiveCompute() 不会被调用如果默认值的计算有开销,一定用 orElseGet。
三、ifPresent 和 ifPresentOrElse
// ifPresent:有值时执行某个动作
optUser.ifPresent(user -> log.info("用户登录: {}", user.getName()));
// ifPresentOrElse(Java 9+):有值和无值都有处理
optUser.ifPresentOrElse(
user -> log.info("用户登录: {}", user.getName()),
() -> log.warn("未找到用户: id={}", id)
);四、filter:条件过滤
// 查找活跃的、且邮箱包含某关键字的用户
Optional<User> activeUser = userRepository.findById(id)
.filter(User::isActive)
.filter(u -> u.getEmail().contains("@company.com"));五、or(Java 9+):备用 Optional
// 如果主来源没找到,试备用来源
Optional<User> user = primaryCache.getUser(id)
.or(() -> secondaryCache.getUser(id)) // 如果主缓存没有,查备缓存
.or(() -> userRepository.findById(id)); // 如果缓存都没有,查数据库这个比 orElse 更灵活,因为备用的也是 Optional,而不是一个固定值。
六、它怎么改变了我的代码风格
这个变化是潜移默化的,说两个例子。
例子1:Service 层的方法设计
以前我的 Repository 层:
User findById(Long id); // 找不到返回 null,调用者可能忘了判空改成:
Optional<User> findById(Long id); // 强迫调用者处理"不存在"的情况这个改变看起来很小,但它在代码评审的时候能发现很多"调用了但没判空"的潜在 bug。因为如果你拿到 Optional<User> 不做任何处理就当 User 用,编译器就会报错。
例子2:业务逻辑的表达
以前:
public String getDisplayName(Long userId) {
User user = userRepository.findById(userId);
if (user == null) {
return "访客";
}
if (user.getNickname() == null || user.getNickname().isEmpty()) {
return user.getUsername();
}
return user.getNickname();
}改成(用 Optional 的链式写法):
public String getDisplayName(Long userId) {
return userRepository.findById(userId)
.flatMap(user -> Optional.ofNullable(user.getNickname())
.filter(nick -> !nick.isEmpty()))
.orElseGet(() ->
userRepository.findById(userId)
.map(User::getUsername)
.orElse("访客"));
}等等,这个写法其实查了两次数据库,不对。更好的:
public String getDisplayName(Long userId) {
return userRepository.findById(userId)
.map(user -> {
// 有 nickname 就用 nickname,否则用 username
String nick = user.getNickname();
return (nick != null && !nick.isEmpty()) ? nick : user.getUsername();
})
.orElse("访客");
}这个版本一次查询,逻辑清晰,不会 NPE。
我承认,有些业务逻辑用 Optional 链写出来不一定比 if-else 更清晰,这时候就用 if-else。工具是服务于可读性的,不是反过来。
七、几个常见的错误用法
错误1:在 Optional 为空时用 get()
Optional<User> opt = Optional.empty();
User user = opt.get(); // NoSuchElementException!
// 永远不要直接用 get(),除非你100%确定有值(那就别用 Optional)错误2:Optional.of() 传入可能为 null 的值
String maybeNull = possiblyNullMethod();
Optional<String> opt = Optional.of(maybeNull); // 如果 maybeNull 是 null,NullPointerException!
// 应该用 Optional.ofNullable()
Optional<String> opt = Optional.ofNullable(maybeNull);错误3:序列化 Optional
// Optional 不实现 Serializable,不能序列化
// 不要把 Optional 放进需要序列化的对象里(比如存 Redis、Session)
class UserSession implements Serializable {
private Optional<String> token; // 运行时会报错!
}错误4:嵌套 Optional
// 坏味道:Optional<Optional<T>>
Optional<Optional<String>> nested = Optional.of(Optional.of("hello"));
// 应该用 flatMap 避免嵌套Optional 用好之后,代码里的 null check 明显减少了,但更重要的是,它促进你在设计 API 的时候就把"这个值可能不存在"显式化,而不是留个隐患给调用方。
下一篇写 Java Record 和 Sealed Class,这两个是 Java 17 引入的语法,工程价值比很多人想象的高。
