Java8 中引入了一个特别有意思类:Optional,一个可以让我们更加轻松的避免 NPE(空指针异常,NullPointException)的工具。
很久很久以前,为了避免 NPE,我们会写很多类似if (obj != null) {}的代码,有时候忘记写,就可能出现 NPE,造成线上故障。在 Java 技术栈中,如果谁的代码出现了 NPE,有极大的可能会被笑话,这个异常被很多人认为是低级错误。Optional的出现,可以让大家更加轻松的避免因为低级错误被嘲讽的概率。 定义示例数据 先定义待操作对象,万能的Student类和Clazz类(用到了 lombok 和 guava):
@Data@AllArgsConstructor@NoArgsConstructorpublic class Clazz { private String id; private String name;}
@Data@AllArgsConstructor@NoArgsConstructorpublic class Student { private String id; private String name; private Clazz clazz;}
然后定义一组测试数据:
final Clazz clazz1 = new Clazz("1", "高一一班"); final Student s1 = new Student("1", "张三", clazz1);final Student s2 = new Student("2", "李四", null); final List students = Lists.newArrayList(s1, s2);final List emptyStudents = Lists.newArrayList();final List nullStudents = null;
创建实例:of、ofNullable 为了控制生成实例的方式,也是为了收紧空值Optional的定义,Optional将构造函数定义为private。想要创建Optional实例,可以借助of和ofNullable两个方法实现。
这两个方法的区别在于:of方法传入的参数不能是null的,否则会抛出NullPointerException。所以,对于可能是null的结果,一定使用ofNullable。
代码如下:
Optional.of(students);Optional.of(emptyStudents);Optional.ofNullable(nullStudents);
Optional类中还有一个静态方法:empty,这个方法直接返回了内部定义的一个常量Optional<?> EMPTY = new Optional<>(),这个常量的value是null。ofNullable方法也是借助了empty实现null的包装:
public static Optional ofNullable(T value) { return value == null ? empty() : of(value);}
所以说,对于null的Optional包装类,指向的都是相同的实例对象,Optional.empty() == Optional.ofNullable(null)返回的是true。换句话说,空Optional是单例的。
为了方便描述,下文中对值为null的Optional统称为“空Optional”。 获取数据:get Optional的get方法有些坑人,先看下它的源码:
public T get() { if (value == null) { throw new NoSuchElementException("No value present"); } return value;}
也就是说,Optional值为空时,java培训使用get方法将抛出NoSuchElementException异常。如果不想抛出异常,或者能够 100%确定不是空Optional,或者使用isPresent方法判断。
如果能 100%确定不是空Optional,那就没有必要使用Optional包装,直接返回即可。如果需要使用isPresent方法,那就和直接判空没有区别了。所以,无论是第一种情况还是第二种情况,都违背了设计这个类的初衷。 值为空判断:isPresent、ifPresent isPresent用来判断值是否为空,类似于obj != null,ifPresent可以传入一个Consumer操作,当值不为空的时候,会执行Consumer函数。比如:
final Optional<List> nullValue = Optional.ofNullable(nullStudents); if (nullValue.isPresent()) { System.out.println("value: " + nullValue.get());}
上面的方法等价于:
nullValue.ifPresent(value -> System.out.println("value: " + value));
isPresent判断的写法上是不是感觉很熟悉,感觉可以直接写为:
if (nullStudents != null) { System.out.println("value: " + nullStudents);}
对于isPresent,如果是在自己可控的代码范围内,完全没有必要将值封装之后再判空。对于自己不可控的代码,后续的filter或者map方法可能比isPresent更好用一些。
对于ifPresent,在使用的时候会有一些限制,就是必须是非空Optional的时候,在会执行传入的Consumer函数。 值处理:map、flatMap map和flatMap是对Optional的值进行操作的方法,区别在于,map会将结果包装到Optional中返回,flatMap不会。但是两个方法返回值都是Optional类型,这也就要求,flatMap的方法函数返回值需要是Optional类型。
我们来看看map的实现:
public Optional map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); }}
可以看到,如果Optional的值为空,map直接返回Optional.EMPTY,否则会执行函数结果,并使用Optional.ofNullable包装并返回。也即是说,只要类结构允许,我们可以一直map下去,就像是扒洋葱,一层一层,直到核心。
比如,我们要获取s2所在班级名称,在定义的时候,我们将s2的clazz属性定义为 null,如果以前需要写为:
String clazzNameOld;if (s2 != null && s2.getClazz() != null && s2.getClazz().getName() != null) { clazzNameOld = s2.getClazz().getName();} else { clazzNameOld = "DEFAULT_NAME";}
现在借助Optional可以写为:
final String clazzName = Optional.ofNullable(s2) .map(Student::getClazz) .map(Clazz::getName) .orElse("DEFAULT_NAME");
从代码上似乎没有多大改变,但是如果Clazz内部还有类对象。或者,我们在if判断的时候,少写一层检查呢?而且,map的精巧还在于它的返回值永远是Optional,这样,我们可以重复调用map方法,而不需要中间被打断,增加各种判空逻辑。 值为空的处理:orElse、orElseGet、orElseThrow 这几个方法可以与map操作结合,一起完成对象操作。当值为空时,orElse和orElseGet返回默认值,orElseThrow抛出指定的异常。
orElse和orElseGet的区别是,orElse方法传入的参数是明确的默认值,orElseGet方法传入的参数是获取默认值的函数。如果默认值的构造过程比较复杂,需要经过一系列的运算逻辑,那一定要使用orElseGet,因为orElseGet是在值为空的时候,才会执行函数,并返回默认值,如果值不为空,则不会执行函数,相比于orElse而言,减少了一次构造默认值的过程。
同样以上面的例子:
orElse的写法:
final String clazzName = Optional.ofNullable(s2) .map(Student::getClazz) .map(Clazz::getName) .orElse(null);
orElseGet的写法:
final String clazzName = Optional.of(s2) .map(Student::getClazz) .map(Clazz::getName) .orElseGet(() -> null);
如果clazz属性一定不为空,为空则返回异常,可以使用orElseThrow:
final String clazzName = Optional.of(s2) .map(Student::getClazz) .map(Clazz::getName) .orElseThrow(() -> new IllegalArgumentException("clazz属性不合法"));
条件过滤:filter filter方法提供的是值验证,如果值验证为 true,返回当前值;否则,返回空Optional。比如,我们要遍历students,找到班级属性为空的,打印学生 id:
for (final Student s : students) { Optional.of(s) .filter(x -> x.getClazz() == null) .ifPresent(x -> System.out.println(x.getId()));}
其他:equals、hashCode、toString Optional重写了这三个方法。因为Optional可以认为是包装类,所以还是围绕这被包装的值重写这三个方法。下面给出这三个方法的源码:
public boolean equals(Object obj) { // 同一对象判断 if (this == obj) { return true; } // 类型判断 if (!(obj instanceof Optional)) { return false; } Optional other = (Optional) obj; // 最终还是值的判断 return Objects.equals(value, other.value);} public int hashCode() { // 直接返回值的hashCode return Objects.hashCode(value);} public String toString() { return value != null ? String.format("Optional[%s]", value) // 用到了值的toString结果 : "Optional.empty";}
equals方法,Optional.of(s1).equals(Optional.of(s2))完全等价于s1.equals(s2)。
hashCode方法,直接返回的是值的 hashCode,如果是空Optional,返回的是 0。
toString方法,为了能够识别是Optional,将打印数据包装了一下。如果是空Optional,返回的是字符串“Optional.empty”;如果是非空,返回是是“Optional[值的 toString]”。 文末总结 NPE 之所以讨厌,就是只要出现 NPE,我们就能够解决。但是一旦出现,都已经是事后,可能已经出现线上故障。偏偏在 Java 语言中,NPE 又很容易出现。Optional提供了模板方法,有效且高效的避免 NPE。
接下来,我们针对上面的使用,总结一下:
另外,还有一些实践上的建议: