Optional浅析——优雅地解决空指针异常

143 阅读5分钟

1、平时开发常见的取值方法

Java开发中,获取值的时候最常想到的应该就是直接使用.获取,如:

// 从用户信息实例中的证件信息实例获取用户姓名
userInfo.getIdCardInfo().getName()

这样有时候会造成NPE(NullPointException)这种常见且尴尬的异常,所以为了健壮性,往往会做如下改造:

String name = "";
if (userInfo != null && userInfo.getIdCardInfo() != null) {
    name = userInfo.getIdCardInfo().getName();
}
System.out.println(name);

甚至,如果新需求是当name为空时,应该抛出个错误,那必不可少要多一个判断:

String name = "";
if (userInfo != null && userInfo.getIdCardInfo() != null) {
    name = userInfo.getIdCardInfo().getName();
}
System.out.println(name);
if (StringUtils.isEmpty(name)) {
    throw new RuntimeException("哎呀,姓名参数是空的。");
}

2、Optional如何优雅的处理NPE

先上代码

Optional<UserInfo> userInfoOptional = Optional.ofNullable(userInfo);
String name = userInfoOptional.map(UserInfo::getIdCardInfo)
    .map(IDCardInfo::getName)
    .orElseThrow(() -> new RuntimeException("哎呀,姓名参数是空的。"));

惊不惊喜意不意外??两行代码就能搞定非空判断 && 取值 && 异常抛出,甚至可以将代码简化为一行

Optional.ofNullable(userInfo)
    .map(UserInfo::getIdCardInfo)
    .map(IDCardInfo::getName)
    .orElseThrow(() -> new RuntimeException("哎呀,姓名参数是空的。"));

没有if-else做非空判断,使代码看起来更加地优雅简洁。如果代码暂时看不懂,没关系,我们继续探究Optional中的各个方法。

3、Optional方法介绍

3.1 构造方法

Optional类的构造方法为private,所以不能直接通过new得到一个Optional实例。Optional为我们提供了三种创建实例的静态方法。

(1)empty()

private static final Optional<?> EMPTY = new Optional<>();
/**
  *  返回一个空的Optional实例,空实例即为Optional不包含值
  */
public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

(2)of(T value)

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

可以构造一个包含非空(non-null)值的Optional实例,如果传入的值为null,会抛出NPE异常

(3)ofNullable(T value)

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

由源码可知,该静态方法根据传入值是否为null进行三目运算,分情况调用empty()of(value)。所以传入值为null时,也不是真的为null,而是创建一个不包含值的Optional容器。

3.2 相关方法

(1)map() && flatMap()

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    // 非空判断,如果为空,直接抛出NPE异常
    Objects.requireNonNull(mapper);
    // 如果值为空,返回不包含值的Optional实例
    if (!isPresent())
        return empty();
    else {
        // 否则返回使用Optional包装的值
        return Optional.ofNullable(mapper.apply(value));
    }
}

如果当前的Optional不包含值,即empty(),则继续返回不包含值的Optional。否则,返回一个新的Optional,该Optional包含mapper函数输入的值。例如上边的例子:

Optional.ofNullable(userInfo)
    // 返回IdCardInfo对象,且使用Optional包装,即为Optional<IDCardInfo>
    .map(UserInfo::getIdCardInfo)
    // 返回使用Optional包装的String字符串,即为Optional<String>
    .map(IDCardInfo::getName)
    .orElseThrow(() -> new RuntimeException("哎呀,姓名参数是空的。"));

**注意:**如果输入的值已经被Optional包装,即userInfo.getIdCardInfo()返回的是Optional<IdCardInfo>,那么使用map()后,返回的值将为Optional<Optional<IdCardInfo>>

那么如何避免上面这种取值出现嵌套两层Optional的情况呢,就要使用我们接下来讲的flatMap()方法。

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

从源码可以看出,flatMap()map()方法基本一致,只是返回的时候,不再使用Optional包装,而是将mapper函数输入的值直接返回。所以,当userInfo.getIdCardInfo()返回的是Optional<IdCardInfo>时,使用flatMap()方法,返回的值为Optional<IdCardInfo>

(2)ifPresent() && isPresent()

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

该方法用来判断Optional中是否包含值,如果有,则执行consumer.accept()操作,如果没有,不操作。

Consumer为一个lamda表达式,如:name -> System.out.println(name);

**示例:**如果IdCardInfo实例存在name的值,就将它打印出来

Optional<UserInfo> userInfoOptional = Optional.ofNullable(userInfo);
userInfoOptional.map(UserInfo::getIdCardInfo)
    .map(IDCardInfo::getName)
    // 如果只是单纯打印name值,以下代码可以改写为
    // ifPresent(System.out::println)
    .ifPresent(name -> System.out.println(name));

isPresent()ifPresent()只有一个字母的差别,作用也有所差异,从名字也可以看出,isPresent()是判断Optional是否存在值,没有入参,返回值是boolean类型。

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

(3)filter()

public Optional<T> filter(Predicate<? super T> predicate) {
    // 非空判断
    Objects.requireNonNull(predicate);
    // 如果不包含值,则返回当前对象
    if (!isPresent())
        return this;
    else
        // 如果predicate判断条件与value相同,则返回当前对象,否则返回empty()
        return predicate.test(value) ? this : empty();
}

filter()与Stream流的filter()作用类似,都是用来筛选符合条件的数据。这里的filter()如果值符合传入的条件,则返回当前对象,否则返回一个不包含值的Optional实例。

**示例:**如果IdCardInfo实例存在nameyostar的值,就将它打印出来

Optional<UserInfo> userInfoOptional = Optional.ofNullable(userInfo);
userInfoOptional.map(UserInfo::getIdCardInfo)
    .map(IDCardInfo::getName)
    // 以下代码也可以为:filter(name -> "yostar".equals(name))
    .filter("yostar"::equals)
    .ifPresent(System.out::println);

(4)get()

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

使用get()来获取当前Optional实例的值,如果当前实例不包含值,会抛出NoSuchElementException异常,否则,直接返回该实例包含的值。示例如下

// 获取IdCardInfo中的name值
Optional<UserInfo> userInfoOptional = Optional.ofNullable(userInfo);
String name = userInfoOptional.map(UserInfo::getIdCardInfo)
                .map(IDCardInfo::getName)
                .get();
System.out.println(name);

(5)orElse() && orElseGet()

orElse()orElseGet()类似,都为设置默认值,不同的是orElse()接收的参数是任意类型T,而orElseGet() 接收的参数为一个Supplier函数式接口

Consumer为消费型函数式接口,没有返回值

Supplier为供给型函数式接口,返回值为T

**示例:**如果IdCardInfo实例存在name的值,就将它打印出来,如果不存在,赋予默认值yostar

Optional<UserInfo> userInfoOptional = Optional.ofNullable(userInfo);
String name = userInfoOptional.map(UserInfo::getIdCardInfo)
                .map(IDCardInfo::getName)
//                .orElse("yostar")
                .orElseGet(() -> "yostar");
ystem.out.println(name);

(6)orElseThrow()

该方法与orElse()的区别在于,该方法当Optional中包含值时,返回值,不包含值时,抛出自己传入的异常。

示例: 如文章开头的示例,当IdCardInfo实例中不存在name信息时,抛出异常。

Optional.ofNullable(userInfo)
    .map(UserInfo::getIdCardInfo)
    .map(IDCardInfo::getName)
    .orElseThrow(() -> new RuntimeException("哎呀,姓名参数是空的。"));

4、总结

Optional是Java8开始用来代替NULL的一个容器对象,可以很好很优雅的减少程序的空指针异常。

Optional设计十分精细,很自然的融入Java8支持的函数式编程。