JAVA8如何用Optional解决NPE问题详解
引言
NPE(NullPointerException,空指针异常)是 Java 开发中最常见的异常之一。社区中关于“方法到底应该返回 null 还是返回空对象”的讨论从未停止。
在本文开头,我们先回顾一下 NPE 问题的典型场景。假设我们有两个类,其 UML 类图关系如下图所示:

在这种情况下,如果直接链式调用:
user.getAddress().getProvince();当 user 为 null 时,上述代码极有可能抛出 NullPointerException 异常。为了解决这个问题,传统的写法是进行多层非空判断:
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String province = address.getProvince();
}
}这种写法虽然安全,但代码显得冗余且嵌套过深,影响了可读性。为了避免这种繁琐的空值检查,让代码更加优雅,Java 8 引入了 Optional 类。接下来本文将详细说明如何利用 Optional 优化上述写法。
参考资料:JAVA8 如何妙用 Optional 解决 NPE 问题详解
Optional API 详解
与其他文章不同,本文将采取类比的方式结合源码进行讲解,避免单纯罗列 API 导致重点模糊。
1. 创建 Optional 对象
以下四个方法具有相关性,建议组合记忆:Optional(T value)、empty()、of(T value)、ofNullable(T value)。
Optional(T value):构造函数,权限为private,外部无法直接调用。- 其余三个为
public静态方法,供开发者调用。
Optional 的本质是内部存储了一个真实的值。在构造时,它会判断该值是否为空。查看 Optional(T value) 构造函数的源码如下:

of(T value) 的源码如下:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}即 of(T value) 内部调用了构造函数。根据构造函数逻辑可得出以下结论:
- 通过
of(T value)构造出的Optional对象,如果传入的value为null,会直接抛出NullPointerException。 - 如果
value不为null,则能正常构造Optional对象。
此外,Optional 类内部还维护了一个值为 null 的单例对象,大致结构如下:
public final class Optional<T> {
// 省略部分代码
private static final Optional<?> EMPTY = new Optional<>();
private Optional() {
this.value = null;
}
// 省略部分代码
public static <T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}empty() 方法的作用就是返回这个 EMPTY 对象。
铺垫至此,我们可以理解 ofNullable(T value) 的作用了,其源码如下:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}of(T value) 与 ofNullable(T value) 的区别:
- 当
value为null时,of(T value)会抛出NullPointerException异常。 - 当
value为null时,ofNullable(T value)不会抛异常,而是直接返回一个EMPTY对象。
是否项目中只需用 ofNullable 而不用 of?
并非如此。存在的即合理的。如果在运行过程中,不希望隐藏 NullPointerException,而是希望立即暴露问题,则应使用 of 函数。不过在实际业务开发中,这种场景较少见,作者通常在编写 JUnit 测试用例时才会用到 of 函数。
2. 默认值与异常处理
以下三个方法放一组记忆,都是在构造函数传入的 value 值为 null(即 Optional 为空)时进行调用:orElse(T other)、orElseGet(Supplier<? extends T> other) 和 orElseThrow(Supplier<? extends X> exceptionSupplier)。
orElse 和 orElseGet 的用法如下,相当于当 value 值为 null 时,给予一个默认值:
@Test
public void test() {
User user = null;
// 无论 user 是否为 null,createUser() 都会被执行
user = Optional.ofNullable(user).orElse(createUser());
// 只有当 user 为 null 时,createUser() 才会被执行
user = Optional.ofNullable(user).orElseGet(() -> createUser());
}
public User createUser() {
User user = new User();
user.setName("zhangsan");
return user;
}这两个函数的区别:
当 user 值不为 null 时,orElse 函数依然会执行 createUser() 方法(因为参数是直接传入的值),而 orElseGet 函数并不会执行 createUser() 方法(因为参数是 Supplier 接口,懒加载)。
至于 orElseThrow,则是当 value 值为 null 时,直接抛出一个异常,用法如下:
User user = null;
Optional.ofNullable(user).orElseThrow(() -> new Exception("用户不存在"));3. 值转换:map 与 flatMap
这两个函数用于转换值,建议放在一起记忆。
源码如下:
public final class Optional<T> {
// 省略部分代码
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
// 省略部分代码
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
}这两个函数在逻辑上相似,唯一区别在于入参:
map函数接受的入参类型为Function<? super T, ? extends U>。flatMap的入参类型为Function<? super T, Optional<U>>。
具体用法:
如果 User 结构如下(getName 返回 String):
public class User {
private String name;
public String getName() {
return name;
}
}这时候取 name 的写法如下(使用 map):
String name = Optional.ofNullable(user).map(u -> u.getName()).get();如果 User 结构如下(getName 返回 Optional<String>):
public class User {
private String name;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
}这时候取 name 的写法如下(使用 flatMap):
String name = Optional.ofNullable(user).flatMap(u -> u.getName()).get();4. 判断与消费:isPresent 与 ifPresent
这两个函数放在一起记忆:
isPresent():判断value值是否为空。ifPresent(Consumer<? super T> consumer):在value值不为空时,执行某些操作。
源码如下:
public final class Optional<T> {
// 省略部分代码
public boolean isPresent() {
return value != null;
}
// 省略部分代码
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
}注意: 千万不要将传统的空值判断:
if (user != null) {
// TODO: do something
}写成以下这种“伪优化”的形式:
Optional<User> optUser = Optional.ofNullable(user);
if (optUser.isPresent()) {
// TODO: do something
}因为这样写代码结构依然冗余,并未体现 Optional 的优势。作者会在后文给出正确写法。
至于 ifPresent(Consumer<? super T> consumer),用法很简单,如下所示:
Optional.ofNullable(user).ifPresent(u -> {
// TODO: do something
});5. 过滤:filter
filter 方法接受一个 Predicate 来对 Optional 中包含的值进行过滤。如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty()。
源码逻辑如下:
public final class Optional<T> {
// 省略部分代码
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
}用法如下:
Optional<User> user1 = Optional.ofNullable(user).filter(u -> u.getName().length() < 6);如上所示,如果 user 的 name 长度小于 6,则返回包含该用户的 Optional;如果大于等于 6,则返回一个 EMPTY 对象。
实战案例
案例一:嵌套对象取值
传统写法:
public String getCity(User user) throws Exception {
if (user != null) {
if (user.getAddress() != null) {
Address address = user.getAddress();
if (address.getCity() != null) {
return address.getCity();
}
}
}
throw new Exception("取值错误");
}Java 8 写法:
public String getCity(User user) throws Exception {
return Optional.ofNullable(user)
.map(u -> u.getAddress())
.map(a -> a.getCity())
.orElseThrow(() -> new Exception("取值错误"));
}案例二:条件执行
传统写法:
if (user != null) {
doSomething(user);
}Java 8 写法:
Optional.ofNullable(user)
.ifPresent(u -> {
doSomething(u);
});案例三:过滤与默认值
传统写法:
public User getUser(User user) throws Exception {
if (user != null) {
String name = user.getName();
if ("zhangsan".equals(name)) {
return user;
}
} else {
user = new User();
user.setName("zhangsan");
return user;
}
// 原文逻辑此处可能有缺失,视具体情况补充返回
return null;
}Java 8 写法:
public User getUser(User user) {
return Optional.ofNullable(user)
.filter(u -> "zhangsan".equals(u.getName()))
.orElseGet(() -> {
User user1 = new User();
user1.setName("zhangsan");
return user1;
});
}总结与说明
链式编程虽然使代码更加优雅,但逻辑性可能没那么明显,可读性有所降低。大家在项目中应根据实际情况酌情使用,避免过度设计。
说明:本文基于 Java 8 版本编写。Optional类在后续 Java 版本中功能有所增强(如 Stream 集成等),但核心 API 用法保持一致。在实际开发中,请注意Optional主要用于返回值的包装,不建议用作字段类型或方法参数。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java8-ru-he-yong-optional-jie-jue-npe-wen-ti-xiang-jie.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。