原文地址:http://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html


PS:这里要说两句了,拷贝过来的文章标题清除了格式,保存还是失败了,编辑器的问题吗?感觉编辑器怎么用着很不舒服。


  回归正文,Java空指针异常带来不少问题和麻烦,现在Java8提供了Optional特性,我们可以试试了。


  有大牛说:“如果没有处理过空指针异常(NullPointerException),就不能算是真正的Java程序员”。开个玩笑的说法,不过可以看出空指针异常存在的普遍性,空(null)引用是许多问题的根源,因为经常将null表示一个空值。Java SE8引进了一个新类叫java.util.Optional,它将缓解这些问题的发生。


 下面让我们从一个小例子中看看null的危害性。如下图1示,用一个嵌入对象结构代表一台计算机:

空指针异常处理可是试试Java8的Optional特性_空指针与Optional


  看着上图,看看下面的代码可会会发生什么样的问题呢?

String version=computer.getSoundCard().getUSB().getVersion();


    哈哈,上面一一行代码看起来合情合理(译者注:实际中我们会不经意间的自然流畅的写出这样的代码)。然而,许多计算机(比如:Respberry Pi)实际上没有搭载声卡,所以getSoundCard()返回的结果是什么呢?

   通常的实践(译者注:这也是一个不好的实践,我所在的公司Java开发规范文档中有明确的一条规定就是不能返回null)做法是返回一个空引用来表示没有声卡。不幸的是,这意味着调用getUSB()方法时将试图返回一个空引用的USB端口,最终将返回一个运行时的空指针异常(NullPointerException),并且进一步阻止程序继续运行。想像一下,如果你的程序运行在客户的机器上,毫无疑问,在客户看来你的程序是失败的。


     追随计算机编程的发展历史背景,计算机科学巨匠-Tony Hoare 曾经写道:"在1965年发明了空(null)引用,我把它称为百万美元的错误——我曾经禁不住空引用的诱惑,因为它太容易实现了。"


     我们如何阻止这种无意识的空指针异常呢?你可以主动的添加检查来阻止空引用,如下代码清单1:

String version="UNKNOWN";
if(computer!=null){
    SoundCard soundCard=computer.getSoundCard();
    if(soundCard!=null){
        USB usb=sundCard.getUSB();
        version=usb.getVersion();
    }
}


   然而,你很快就看到了代码清单1中的代码由于嵌套坚持变得丑陋难看。很不幸,我们需要许多这样类似的样板代码来确保不得到一个NullPointerException.此外,我们会抱怨这种检查妨碍了业务逻辑,事实上,它们会使我们的程序可读性下降。

   此外,上面这种方式是一个容易出错的过程,如果你忘记了检查一个可能为null的属性嗯?我在文中用null来表示一个不存在的值是一个错误的方法。我们需要一个更好的方式来表示一个值存在和不存在的模型。


  根据上文,我们简要看看其他编程语言是如何提供这种处理模型的。

有什么可以代替Null呢?

 如Groovy语言有安全导航操作符”?.“表示,其能安全的操作潜在的空引用。(注:Groovy实现之后不久,C#也提供了,有人也在JavaSE7中提议过,但是最终没能发行。),它的工作方式如下:

String version=computer?.getSoundCard()?.getUSB()?.getVersion();

  这种情况下,如果computer, getSoundCard(), getUSB()返回的是null,那么变量version被赋值为null.


   此外Groovy也包含了Elvis操作符"?:",(哈哈,你从侧面看看这个符合,你可能会认为这个猫王的头发)。当需要一个默认值的时候,它可以被用来作为一个简单的例子。如下面代码,如果表达式用安全导航操作符返回null,这个默认值就是”UNKNOWN“,否则它将返回版本号。


String version=computer?.getSoundCard()?.getUSB()?.getVersion()?:"UNKNOWN";  qit


  其它函数语言中,如Haskell和Scala,提出不同的观点。Haskell包含一个Maybe类型,它包括了一个可选值。一个Maybe类型的值包含了指定类型的值或者什么也没有。没有空引用的概念。Scala有一个相似的结构体Option[T]来封装T类型的值存在或不存在。你可以明确的在一个可选类型是进行存在或不存在值检查。当然不能忘记对“null"复杂检查,因为这是系统类型。


   上面说了这么多,听起来真的很抽象,下面看看JavaSE8中是如何处理这个问题的。

Optional 概述:

   JavaSE8引入了一个新的类 java.util.Optional<T>,它的灵感源于Haskell和Scale的启发,它封装了一个可选值。下面代码清代2和图2,你可以把Optional看作一个容器,里面包含一个存在的值或者不存在的值(可选,不可选),如图2.

空指针异常处理可是试试Java8的Optional特性_Java8_02

 下面我们可以用Optional来更新我们的模型,如下代码清代2:

package com.test.jse8;
import java.util.Optional;
public class Computer {
    private Optional<SoundCard> soundCard;
    public Optional<SoundCard> getSoundCard() {
        return this.soundCard;
    }
}
class SoundCard {
    private Optional<USB> usb;
    public Optional<USB> getUsb() {
        return this.usb;
    }
}
class USB {
    private String version;
    public String getVersion() {
        return version;
    }
}


     从上面代码(代码清单2)中可以清楚的看出,一个计算机可能有或者肯能没有声卡(因为可选),此外还能看出声卡的USB端口也是可选的。这是一种金币,这个新的模型清楚的反映出来出来给定的值是否可以不存在,类似的实现在Guava的类库中也有实现。


    但是实际上你并不想去处理Optional<SoundCard>这个对象,你想要获取的是USB端口的版本号。简而言之,Optional类提供了处理值存在或者不存在的方法。相比于空引用的优势在于Optional显式的强制你考虑值不存在的可能情况,这样做的后果是你将避免了空指针异常的发生。


   需要重要的注意一点是Optional并不是替代空引用,相反,它的目的在于帮助设计更可理解的API和可阅读的方法。你可以选择Optional来告诉自己希望一个可选的值,这样迫使你积极的打开一个不存在的值的可选值。


Optional在模式中的应用

  不多废话,上代码。我们先看看null-check模式使用Optional的重写。在文章结尾我们将明白如何使用Optional了。下面是代码清代1中的的null-check重写代码:

String name = computer.flatMap(Computer::getSoundcard)
                          .flatMap(Soundcard::getUSB)
                          .map(USB::getVersion)
                          .orElse("UNKNOWN");


   注:你可以复习一下,Java SE8 lambas和方法应用语法see "Java 8:Lambdas 和 管道流概念see "Process Data with Java SE8 Streams" .


创建Optional对象

首先,怎么创建Optional对象能?下面采用几种方式:

   1.一个空Optional对象:
       Optional<Soundcard> sc = Optional.empty();
   2.Optional对象有一个非空值:
      SoundCard soundcard = new Soundcard();
      Optional<Soundcard> sc = Optional.of(soundcard);
      这里如果soundcard是null,NullPointerException将立即抛出,而不是当访问soundcard的属性是获得一个潜在的错误。

     当然,也可以是用ofNullable,创建一个持有null值的Optional对象:
     Optional<Soundcard> sc = Optional.ofNullable(soundcard);
     这里如果soundcard是null,由此产生的Optional对象的结果将是空。

如果值存在,可以继续做事

    现在有一个Optional对象,你可以访问方法,明确的显示处理值存在与否。以前,我们用null-check,如下:

if(soundCard!=null){
           //do something
           System.out.println(soundCard);
       }


    你可以使用ifPresent()方法,如下:

Computer computer = new Computer();
        Optional<SoundCard> soundCard = computer.getSoundCard();
        if (soundCard.isPresent()) {
            //do something
        }


    你将不用显式的进行null-check,它将有系统类型强制实施。如果Optional对象是空,上述代码中将什么都不打印。

    你可以使用isPresent()方法来在Optional对象中查看值是否存在。此外,如果存在,它的get()方法及将返回Optional中包含的值。否则,它将抛出NoSuchElementException。这两个方法组合使用,能否阻止异常发生。

if(soundcard.isPresent()){
          System.out.println(soundcard.get());
        }


  然而,这是Optional不推荐的用法(因为它与null-check相对并没有多少提升),下面有很多通用的替代方式。


默认值

   一个典型的方式是在你确定操作结果返回是null的情况下设定默认值。通常你可以使用三目运算操作来做到这一点。

Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard
            : new Soundcard("basic_sound_card");


   使用Optional对象,你可以使用orElse()方法来重写上面代码,如果Optional是空的时候,它提供了一个默认值。

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));


  相似的方式,你可以使用orElseThrow()方法,如果Optional是空的时候,它提供一个默认值,即就是一个异常对象。

Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);


PS:

事实上在google的Guava库中已有Optional<T>,而Java7和8也从中借鉴了好多东西。

参见google Guava库的API:http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/Optional.html (有时可能无法访问)可以参考这个:http://api.zhanghang.org/guava-14.0.1-javadoc/com/google/common/base/Optional.html

参见Java 8 SE的API:http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html