利用Optional解决空指针异常

11,065 阅读6分钟

背景介绍

Java 8 引入了一个十分实用的 Optional 类,它主要是为了解决空指针异常(NullPointerException)。当我们对对象的属性进行检查,判断它的值是否为期望的格式,最终却发现我们查看的并不是一个对象,而是一个空指针,它会立即抛出一个让人厌烦的 NullPointerException 异常。

本质上,Optional 类是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。

案例

从一个简单的用例开始。在 Java 8 之前,任何访问对象方法或属性的调用都可能导致 NullPointerException:

String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

为了确保不触发异常,需要在访问每一个值之前对其进行明确地检查:

if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            if (isocode != null) {
                isocode = isocode.toUpperCase();
            }
        }
    }
}

为了简化这个过程,我们使用 Optional 优化这些代码。

基本用法解析

Optional 的对象可能包含值,也可能为空。可以使用同名方法创建一个空的 Optional。

Optional<User> emptyOpt = Optional.empty();
emptyOpt.get();

此时访问 emptyOpt 变量的值会导致 NoSuchElementException。

还可以使用 of()ofNullable() 方法创建包含值的 Optioanal 实例,区别在于如果将 null 当作参数传进去 of() 会报空指针异常,所以当对象可能存在或者不存在,应该使用 ofNullable()。

Optional<User> opt = Optional.of(user);
Optional<User> opt = Optional.ofNullable(user);

从 Optional 实例中获得实际值对象的方法之一是使用 get() 方法。

String name = "John";
Optional<String> opt = Optional.ofNullable(name);
assertEquals("John", opt.get());

这个方法会在值为 null 的时候抛出异常。要避免异常,需要首先用 isPresent() 方法验证是否有值。

User user = new User("zjh@gmail.com", "1234");
Optional<User> opt = Optional.ofNullable(user);
assertTrue(opt.isPresent());
assertEquals(user.getEmail(), opt.get().getEmail());

该方法除了执行检查,还接受一个 Consumer(消费者) 参数,如果对象不是空的,就对执行传入的 Lambda 表达式。

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));

ifPresent() 方法外,Optional 类提供了 API 用以返回对象值,或者在对象为空的时候返回默认值。

可以使用的方法是 orElse() ,它的工作方式非常直接,如果有值则返回该值,否则返回传递给它的参数值。

User user = null;
User user2 = new User("zjh@gmail.com", "1234");
User result = Optional.ofNullable(user).orElse(user2);
assertEquals(user2.getEmail(), result.getEmail());

此时 user 对象为 null,result 返回 orElse() 参数 user2,如果对象初始值不是 null,那么默认值会被忽略。

第二个同类型的 API 是 orElseGet() —— 其行为略有不同。这个方法会在有值的时候返回值,如果没有值,它会执行作为参数传入的 Supplier(供应者) 函数式接口,并将返回其执行结果:

User result = Optional.ofNullable(user).orElseGet( () -> user2);

orElse()orElseGet() 的不同之处在于当 ofNullable() 传入参数不为空时,orElse() 方法仍然创建了 User 对象。与之相反,orElseGet() 方法不创建 User 对象。

在执行较密集的调用时,比如调用 Web 服务或数据查询,这个差异会对性能产生重大影响

除了 orElse()orElseGet() 方法,Optional 还定义了 orElseThrow() API —— 它会在对象为空的时候抛出异常,而不是返回备选的值:

User result = Optional.ofNullable(user)
    .orElseThrow( () -> new IllegalArgumentException());

这里,如果 user 值为 null,会抛出 IllegalArgumentException

这个方法让我们有更丰富的语义,可以决定抛出什么样的异常,而不总是抛出 NullPointerException

转换与过滤

通过 map()flatMap() 方法可以转换 Optional 的值。

User user = new User("zjh@gmail.com", "1234");
String email = Optional.ofNullable(user)
    .map(u -> u.getEmail()).orElse("default@gmail.com");
assertEquals(email, user.getEmail());

map() 对值应用(调用)作为参数的函数,然后将返回的值包装在 Optional 中。这就使对返回值进行链式调用的操作成为可能 —— 这里的下一环就是 orElse()

flatMap() 也需要函数作为参数,并对值调用这个函数,然后直接返回结果

除了转换值之外,Optional 类也提供了按条件“过滤”值的方法。

filter() 接受一个 Predicate 参数,返回测试结果为 true 的值。如果测试结果为 false,会返回一个空的 Optional。

User user = new User("zjh@gmail.com", "1234");
Optional<User> result = Optional.ofNullable(user)
    .filter(u -> u.getEmail() != null && u.getEmail().contains("@"));
assertTrue(result.isPresent());

案例优化

使用 Optional 重写最早介绍的案例,删除 null 检查,替换为 Optional 的方法。

User user = new User("zjh@gmail.com", "1234");
String result = Optional.ofNullable(user)
    .map(u -> u.getAddress())
    .map(a -> a.getCountry())
    .map(c -> c.getIsocode())
    .orElse("default");
assertEquals(result, "default");

源码解析

private Optional() {
    this.value = null;
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

可以看到源码中的构造函数全部都被私有化, 我们能够通过工厂方法的方式来获取 Optional 包装类的实例。

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

上面的方法是不允许传 null 值的, 下面的是可以的,下面的方法传了空值, 它会默认的帮你创建一个空的 Optional 包装类对象。

isPresent() 是判断这个包装类是否为空, get() 是获取到被包装的对象。isPresent() 还可以接受一个 Consumer(消费者) 参数,如果对象不是空的,就对执行传入的 Lambda 表达式

public boolean isPresent() {
    return value != null;
}

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

以下三个方法大致上相似, 如果被包装类为空, 一个是直接返回一个新的被包装对象, 一个是通过函数式编程接口 Supplier 返回一个新的被包装类对象, 最后一个直接返回一个指定异常。

public T orElse(T other) {
    return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

以下三个方法接受 Predicate 接口,实现转换与过滤操作。

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}

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));
    }
}

总结

Optional 是 Java 语言的有益补充 —— 它旨在减少代码中的 NullPointerExceptions

通过设计,自然的融入了 Java 8 函数式支持。

总的来说,这个简单而强大的类有助于创建简单、可读性更强、比对应程序错误更少的程序。

加入我们

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

扫码发现职位&投递简历

官网投递:job.toutiao.com/s/FyL7DRg