引言
Optional意为可选,我们前面已经提及过,主要是为了替代null的使用,避免空指针异常(NullPointerException)的出现。譬如定义下面一个类A:
class A {
private String name;
public String getName() {
return name;
}
如果我调用它的getName方法获取name字段并进行后续操作,就将会发生异常。因为我并没有为A写一个构造方法,所以name字段将会为空,如果此时对结果进行操作,比如调用length方法,将会抛出一个空指针异常,这就会很恼人。在没有Optional接口之前,我们常用的避免异常发生的方法是添加一个硬性检查。
例4.0:
String name=new A().getName;
if(name!=null)
System.out.println(name.length());
空城流云 Optional接口
上述代码并不是一个美观的写法,因为总是要在执行命令前进行检查,就好像套了一个try-catch块一样冗余丑陋。为了可以消除这样的样板代码,J8提供了Optional接口,利用它可以写出函数式风格的代码来,比如下面的变形。
例4.1:
Optional<A> oa=Optional.ofNullable(new A());
oa.map(A::getName)
.map(String::length)
.ifPresent(System.out::println);
这样的写法看起来就好像是我们最先学过的Stream类的风格,连方法名都很相似。我们一个一个来分析,最上面的ofNullable工厂方法负责生产一个Optional<A>类型的对象,这样的方法一共有三个,除了ofNullable还有of、和empty。of方法接受一个A类型的参数,并将其装箱成Optional<A>类型的对象,如果传进来的参数为空,则会直接抛出一个NullPointerException,确保被装箱的对象非空(这里我姑且乱用“装箱”和“拆箱”这两个术语,因为Optional对象很像是把一个对象放到了箱子里,当然它也可能会是一个空箱子)。empty则正好相反,调用后直接生成一个内容为空的Optional对象,这使得该对象的功能有点像null,但实际上差别很大,要不然Optional类还有什么意义呢?最后是我们上面使用的ofNullable方法,顾名思义,就是可以传进来一个可空的A类型参数,结合了上面两个方法的功能,相比于of方法,此方法在传入值为null为参数时并不会抛出异常,而是直接生成一个空内容的Optional<A>对象。
生成Optional后,我们又继续调用了一个为map的方法,它与Stream中的map方法很相似,都是对泛型对象进行映射。上例中我们按照A::getName方法所对应的映射规则,将一个Optional<A>类型的对象映射成了一个Optional<String>类型的对象,紧接着又将其映射成了一个Optional<Integer>类型的对象。除了map外,Optional还有filter和flatmap两个方法与Stream很相似。filter很好理解,传入一个Predicate类型的参数来对Optional对象进行所谓的筛选,如果符合条件就保持不变返回对象本身,否则将会返回一个空内容的Optional。flatMap也很好理解,在Stream中它负责将映射后的对象整合成一个流,而在这里,它的作用是将Optional对象拆成一层包装的形式,比如对于下面这个类:
class B {
private A a;
public Optional<A> getA() {
return Optional.of(a);
}
如果我们想要获取new B().getA().getName(),直接调用两次map方法是不行的。因为我们为了防止成员a为null,进而导致空指针异常,使用了Optional<A>类型的返回值。因此直接对Optional<B>对象按照getA方法进行映射操作,会得到一个Option<Option<A>>类型的对象,拆箱后的结果是一个Optional<A>而不是A,所以无法按照getName方法进行映射。为此我们需要使用flatMap,此方法会自动进行拆箱,得到一个只有一层包装的Optional<A>对象。譬如下面的代码,通过flatMap及后续方法,获取含有Name为字母“a”开头字符串的A字段的B对象。
例4.2:
String s = Optional.ofNullable(new B())
.flatMap(B::getA)
.map(A::getName)
.filter(str->str.startsWith("a"))
.get();
这里我们先通过flatMap方法获取了Optional<A>对象,在通过map方法获取了Optional<String>对象,然后通过filter方法进行判断,如果箱内的字符串不以字母“a”开头,则返回一个空箱。上述代码的最后我们调用了一个get方法,这是Optional类最常用的一个方法,直接获取箱内的值,如果是空箱,则会返回null。如果我们想要对s进行标准输出,那么我们会收到一条无此元素异常(NoSuchElementException)而不是空指针异常,不过是换了个异常名字,这使得Optional类看起来好像没毛用,为了避免异常我们还是要进行“!=null”形式的判断,不过这次可以直接用Optional的方法来执行。
例4.3:
Optional<String> os = Optional.ofNullable(new B())
.flatMap(B::getA)
.map(A::getName)
.filter(str->str.startsWith("a"))
.ifPresent(System.out::println);
上述代码中最后调用的就是我们一直留着没讲的ifPresent方法,这个方法看名字也很好理解,它传入一个Consumer类型的参数,如果Optional对象存在内容,则消费里面的对象,即对其执行Consumer中对应的操作。此外还有一个类似的isPresent方法,用于判断箱内是否为空。
Schrödinger's null
有些人会类比于Stream的两类方法,将map、flatMap、filter归为一类,get、ifPresent、isPresent归为一类,因为前者返回的是Optional类型的对象,可以进行级联,而后者的返回值则是其他类型,只能用在级联末尾。他们确实都很相似,包括上一章的Collector与后面要讲的CompleteFuture在内,在设计上都是想要体现一种Java8独特的函数式编程风格。
get方法很像Stream中的collect方法,会将Stream<T>中的元素收集为T收集或T数组,get则会将Optinal<T>转化为T对象,前提是Optinal对象内部的T对象不为null。那么一旦T对象为null会怎样呢,前面我们已经说过get方法会抛出无此元素异常,这让Optional类显得很鸡肋。实际上get方法还有几个兄弟,我一直藏着没讲,它们的名字叫做orElse、orElseGet、orElseThrow。orElse方法传入一个T类型的参数,当内容为空时返回该参数作为缺省值,orElseGet则传入一个Supplier对象来提供缺省值。orElseThrow则传入一个Supplier用来生成要抛出的异常,有了它我们对于空值就可以不再满足于默认的空指针异常以及get方法提供的无此元素异常,而可以定制自己的异常,这听起来是不是很疯狂呢?毕竟有时候我们想要体验J8的新功能,又需要抛出定制异常的获取错误信息,那么就可以把二者结合起来,使用Optional的orElseThrow方法来使代码看起来更加有格调。
大试牛刀
学习了Optional接口所有的方法,我们就来实际运用一下,对下面的代码进行一次变形。直接在第一行就写return会给人带来非常爽快的感觉,下面的代码看起来就像是函数式编程语言中的闭包。
例4.4:
//命令式
public String getFirst(String s) {
if (s != null)
if (s.length() != 0)
return s.substring(0, 1);
return "空";
}
//函数式
public String _getFirst(String s){
return Optional.ofNullable(s)
.filter(str->str.length()!=0)
.map(str->str.substring(0,1))
.orElse("空");
}
小结 可选的Optional
说了这么多,可能还是有很多人想问,这个Optional到底有什么用呢,好像很鸡肋不用也罢。就好比get方法,和我直接进行非空检查相比能简洁到哪里去呢,还要在外面在套一层Optional壳,使代码看起来更加繁琐更加费解,而且不断地拆箱装箱也很烦人,还不如用我以前学过的习惯性写法。这个东西其实见仁见智,就好比匿名内部类与Lambda表达式孰优孰劣,不同人有着不同的看法。函数式风格的代码其实是一种思想,或者在这里也可以看成是一种设计模式,比起以前那种冗长的命令式代码,函数式代码看起来更加清晰、玄妙。至于这种风格是否能够长远的发展下去,甚至广泛取代传统写法,历史自有定论,我等只需将这些新思想传播开来,但并不会强制要求所有人都要接受,用与不用还是在于程序员自身的偏好。
最后让我们再来看一串代码,将两个Optional拆箱运算后再返回一个Optional对象,这段代码我就不讲解了,有兴趣的读者可以玩味一番。
例4.5:
public Optional,String> concat(Optional<String>s1,Optional<String>s2){
return s1.flatMap(str1->s2.map(str2->str1.concat(str2)));
}