Java8-Lambda编程[3] Optional类

Optional类我们前面已经提及过,主要是为了替代null的使用,避免空指针异常(NullPointerException)的出现。譬如定义下面一个类A:

class A {
    private String name;
    public String getName() {
        return name;
    }

如果我调用它的getName方法获取name字段并进行后续操作,就将会发生异常。因为我并没有为A写一个构造方法,所以name字段将会为空,如果此时对结果进行操作,比如调用length方法,将会抛出一个空指针异常,这就会很恼人。在没有Optional类之前,我们常用的避免异常发生的方法是添加一个硬性检查:

    String name=new A().getName;
    if(name!=null)
        System.out.println(name.length());

这样的写法我们已经非常习惯了,但这并不是一个美观的写法,因为总是要在执行命令前进行检查,就好像套了一个try-catch块一样的冗余丑陋。现在我们有了Optional类,就可以写出能够体现Java8函数式编程的代码来,比如下面的变形:

    Optional<A> oa=Optional.ofNullable(new A());
    oa.map(A::getName)
      .map(String::length)
      .ifPresent(System.out::println);

这样的写法看起来就好像是我们最先学过的Stream类的风格,连方法名都很相似。我们一个一个来分析,最上面的ofNullable工厂方法负责生产一个Optional类型的对象,这样的方法一共有三个,除了ofNullable还有of、和empty。of接受一个A类型的参数,并将其包装成Optional类型的对象,如果传进来的参数为空,则会直接抛出一个NullPointerException,确保被装箱(这里我姑且乱用“装箱”和“拆箱”这两个术语,因为Optional对象很像是把一个对象放到了箱子里,当然它也可能会是一个空箱子)的对象非空。empty则正好相反,调用后直接生成一个内容为空的Optional对象,这使得该对象的功能有点像null,但实际上差别很大,要不然Optional类还有什么意义呢?最后是我们上面使用的ofNullable方法,顾名思义,就是可以传进来一个可空的A类型参数,结合了上面两个方法的功能,相比于of方法,此方法在传入值为null为参数时并不会抛出异常,而是直接生成一个空内容的Optional对象。 接下来是一个名为map的方法,它与Stream中的map方法很相似,都是对泛型对象进行映射,比如在上一个例子中,我们就按照A::getName方法所对应的映射规则,将一个Optional类型的对象映射成了一个Optional类型的对象,紧接着又将其映射成了一个Optional类型的对象。除了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);
    }

如果我们想要获取b.getA().getName(),直接调用两次map方法是不行的,因为我们为了防止b.a为null导致的空指针异常使用了Optional作为返回值。这样的话,直接对Optional对象按照getA方法进行映射操作会得到一个Option<Option>类型的对象,拆箱后的结果是一个Optional而不是A,所以无法按照getName方法进行映射。为此我们需要使用flatMap,此方法会自动进行拆箱,得到一个只有一层包装的Optional对象。譬如下面的代码,获取含有Name为字母“a”开头字符串的A字段的B对象:

    String s = Optional.ofNullable(new B())
            .flatMap(B::getA)
            .map(A::getName)
            .filter(str->str.startsWith("a"))
            .get();

这里我们先通过flatMap方法获取了Optional对象,在通过map方法获取了Optional对象,然后通过filter方法进行判断,如果箱内的字符串不以字母“a”开头,则返回一个空箱。 上述代码的最后我们调用了一个get方法,这是Optional类最常用的一个方法,直接获取箱内的值,如果是空箱,则会返回null。如果我们想要对s进行标准输出,那么我们会收到一条无此元素异常(NoSuchElementException)而不是空指针异常,不过是换了个异常名字,这使得Optional类看起来好像没毛用,为了避免异常我们还是要进行“!=null”形式的判断,不过这次可以直接用Optional的方法来执行:

    Optional<String> os = Optional.ofNullable(new B())
            .flatMap(B::getA)
            .map(A::getName)
            .filter(str->str.startsWith("a"));
    if(os.isPresent())
    System.out.println(os.get());

这里我们显式调用isPresent方法来判断上面过滤后得到的Optional对象是否有内容,如果你用的是最新版的Intellij IDEA,那么它会智能地提示你最后两行代码看起来很冗余,按下Alt+Enter键(我的电脑是这样)后它会自动将这两行代码转换为一行,级联后可得:

    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中对应的操作。 有些人会类比于Stream的两类方法,将map、flatMap、filter归为一类,get、ifPresent、isPresent归为一类,因为前者返回的是Optional类型的对象,可以进行级联,而后者则是其他类型的对象,只能用在级联末尾。这些方法和Stream中的方法很像,因为它们都想要体现同一种Java独特的函数式编程风格。 get方法很像Stream中的collect方法,会将Stream对象收集为T对象,get则会将Optinal转化为T对象,前提是Optinal对象内部的T对象不为null。那么一旦T对象为null会怎样呢,前面我们已经说过get方法会抛出无此元素异常,这让Optional类显得很鸡肋,实际上get方法还有几个兄弟,我一直藏着没讲,它们的名字叫做orElse、orElseGet、orElseThrow。orElse传入一个T类型的参数,当内容为空时返回该参数作为缺省值。orElseGet传入一个Supplier对象来提供缺省值。orElseThrow则传入一个Supplier用来生成要抛出的异常,这样的话我们对于空值就可以不再满足于默认的空指针异常于get方法提供的无此元素异常,而可以定制自己的异常,这听起来是不是很疯狂呢。毕竟有时候我们想要体验Java8的新功能,有需要抛出定制异常的获取错误信息,那么就可以把二者结合起来,使用Optional的orElseThrow方法来使代码看起来更加有格调。 学习了所有的方法,我们就来实际运用一下,对下面的代码进行一次变形,直接在第一行就写return会给人带来非常爽快的感觉,下面的代码看起来有点类似函数式编程语言中的闭包:

//命令式
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到底有什么用呢,好像不用也罢,就好比get方法,和我直接进行非空检查相比能简洁到哪里去呢,还要在外面在套一层Optional壳,使代码看起来更加繁琐更加费解,而且不断地拆箱装箱也很烦人,还不如用我以前学过的习惯性写法。这个东西其实见仁见智,就好比匿名内部类与Lambda表达式孰优孰劣,不同人有着不同的看法。函数式风格的代码其实是一种思想,或者在这里也可以看成是一种设计模式,比起以前那种冗长的命令式代码,函数式代码看起来更加清晰、玄妙。至于这种风格是否能够长远的发展下去,甚至广泛取代传统写法,历史自有定论,我等棋子螺钉还只能是将这些新思想传播开来,但并不会强制要求所有人都要接受,用与不用还是在于程序员自身的偏好。 最后让我们再来看一串代码,将两个Optional拆箱运算后再返回一个Optional对象,这段代码我就不讲解了,有兴趣的读者可以玩味一番:

public Optional<String> concat(Optional<String>s1,Optional<String>s2){
    return s1.flatMap(str1->s2.map(str2->str1.concat(str2)));
}