引言

NPE(NullPointerException,空指针异常)是 Java 开发中最常见的异常之一。社区中关于“方法到底应该返回 null 还是返回空对象”的讨论从未停止。

在本文开头,我们先回顾一下 NPE 问题的典型场景。假设我们有两个类,其 UML 类图关系如下图所示:

UML 类图示意

在这种情况下,如果直接链式调用:

user.getAddress().getProvince();

usernull 时,上述代码极有可能抛出 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) 内部调用了构造函数。根据构造函数逻辑可得出以下结论:

  1. 通过 of(T value) 构造出的 Optional 对象,如果传入的 valuenull,会直接抛出 NullPointerException
  2. 如果 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) 的区别:

  • valuenull 时,of(T value) 会抛出 NullPointerException 异常。
  • valuenull 时,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)

orElseorElseGet 的用法如下,相当于当 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);

如上所示,如果 username 长度小于 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 主要用于返回值的包装,不建议用作字段类型或方法参数。