使用Java 8 Optional避免空指針異常

Optional可讓你的代碼具備可讀性,且會避免出現空指針異常。java

都說沒有遇到過空指針異常的程序員不是Java程序員,null確實引起過不少問題。Java 8中引入了一個叫作java.util.Optional的新類能夠避免null引發的諸多問題。程序員

咱們看看一個null引用能致使哪些危害。首先建立一個類Computer,結構以下圖所示:安全

輸入圖片說明
Computer類模型

當咱們調用以下代碼會怎樣?微信

String version = computer.getSoundcard().getUSB().getVersion();
複製代碼

上述代碼看似是沒有問題的,可是不少計算機(好比,樹莓派)實際上是沒有聲卡的,那麼調用getSoundcard()方法可定會拋出空指針異常了。app

一個常規的可是很差的的方法是返回一個null引用來表示計算機沒有聲卡,可是這就意味着會對一個空引調用getUSB()方法,顯然會在程序運行過程當中拋出控制異常,從而致使程序中止運行。想一想一下,當你的程序在客戶端電腦上運行時,忽然出現這種錯是多尷尬的一件事?函數

偉大計算機科學Tony Hoare曾經寫到:"我認爲null引用從1965年被創造出來致使了十億美圓的損失。當初使用null引用對我最大的誘惑就是它實現起來方便。"ui

那麼該怎麼避免在程序運行時會出現空指針異常呢?你須要保持警戒,而且不斷檢查可能出現空指針的狀況,就像下面這樣:spa

String version = "UNKNOWN";
if(computer != null)
    {
        Soundcard soundcard = computer.getSoundcard();
        if(soundcard != null){
             USB usb = soundcard.getUSB();
             if(usb != null){
                 version = usb.getVersion();
                }
            }
    }
複製代碼

然而,你能夠看到上述代碼有太多的null檢查,整個代碼結構變得很是醜陋。可是咱們又不得不經過這樣的判斷來確保系統運行時不會出現空指針。若是在咱們的業務代碼中出現大量的這種空引用判斷簡直讓人惱火,也致使咱們代碼的可讀性會不好。設計

若是你忘記檢查要給值是否爲空,null引用也是存在很大的潛在問題。這篇文章我將證實使用null引用做爲值不存在的表示是很差的方法。咱們須要一個更好的表示值不存在的模型,而不是再使用null引用。指針

Java 8引入了一個新類叫作java.util.Optional<T>,這個類的設計的靈感來源於Haskell語言和Scala語言。這個類能夠包含了一個任意值,像下面圖和代碼表示的那樣。你能夠把Optional看作是一個有可能包含了值的值,若是Optional不包含值那麼它就是空的,下圖那樣。

輸入圖片說明
Optional模型

public class Computer {
  private Optional<Soundcard> soundcard;
  public Optional<Soundcard> getSoundcard() { ... }
  ...
}

public class Soundcard {
  private Optional<USB> usb;
  public Optional<USB> getUSB() { ... }

}

public class USB{
  public String getVersion(){ ... }
}
複製代碼

上述代碼展示了一臺計算機有可能包換一個聲卡(聲卡是有可能存在也有可能不存在)。聲卡也是有可能包含一個USB端口的。這是一種改善方法,該模型能夠更加清晰的反映一個被給定的值是能夠不存在的。

可是該怎麼處理Optional<Soundcard>這個對象呢?畢竟,你想要獲取的是USB的端口號。很簡單,Optional類包含了一些方法來處理值是否存在的情況。和null引用相比Optional類迫使你在你要作值是否相關處理,從而避免了空指針異常。

須要說明的是Optional類並非要取代null引用。相反地,是爲了讓設計的API更容易被理解,當你看到一個函數的簽名時,你就能夠判斷要傳遞給這個函數的值是否是有可能不存在。這就促使你要打開Optional類來處理確實值的情況了。

採用Optional模式

囉嗦了這麼多,來看一些代碼吧!咱們先看一下怎麼使用Optional改寫傳統的null引用檢測後是什麼樣子。在這邊文章的末尾你將會明白怎麼使用Optional。

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

建立Optional對象

能夠建立一個空的Optional對象:

Optional<Soundcard> sc = Optional.empty();
複製代碼

接下來是建立一個包含非null值的Optional:

SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
複製代碼

若是聲卡null,空指針異常會當即被拋出(這比在獲取聲卡屬性時才拋出要好)。

經過使用ofNullable,你能夠建立一個可能包含null引用的Optional對象:

Optional<Soundcard> sc = Optional.ofNullable(soundcard);
複製代碼

若是聲卡是null 引用,Optional對象就是一個空的。

對Optional中的值的處理

既然如今已經有了Optional對象,你能夠調用相應的方法來處理Optional對象中的值是否存在。和進行null檢測相比,咱們可使用ifPresent()方法,像下面這樣:

Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);
複製代碼

這樣就沒必要再作null檢測,若是Optional對象是空的,那麼什麼信息將不會打印出來。

你也可使用isPresent()方法查看Optional對象是否真的存在。另外,還有一個get()方法能夠返回Optional對象中的包含的值,若是存在的話。不然會拋出一個NoSuchElementException異常。這兩個方式能夠像下面這樣搭配起來使用,從而避免異常:

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

複製代碼

可是這種方式不推薦使用(它和null檢測相比沒有什麼改進),下面咱們將會探討一下工做慣用的方式。

返回默認值和相關操做

當遇到null時一個常規的操做就是返回一個默認值,你可使用三元表達式來實現:

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

複製代碼

使用Optional對象的話,你能夠orElse()使用重寫,當Optional是空的時候orElse()能夠返回一個默認值:

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
複製代碼

相似地,當Optional爲空的時候也可使用orElseThrow()拋出異常:

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

複製代碼

使用filter過濾特定的值

咱們經常會調用一個對象的方法來判斷它的一下屬性。好比,你可能須要檢測USB端口號是不是某個特定值。爲了安全起見,你須要檢查指向USB的醫用是不是null,而後再調用getVersion()方法,像下面這樣:

USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
  System.out.println("ok");
}
複製代碼

若是使用Optional的話可使用filter函數重寫:

Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
                    .ifPresent(() -> System.out.println("ok"));

複製代碼

filter方法須要一個predicate對向做爲參數。若是Optional中的值存在而且知足predicate,那麼filter函數將會返回知足條件的值;不然,會返回一個空的Optional對象。

使用map方法進行數據的提取和轉化

一個常見的模式是提取一個對象的一些屬性。好比,對於一個Soundcard對象,你可能須要獲取它的USB對象,而後判斷它的的版本號。一般咱們的實現方式是這樣的:

if(soundcard != null){
  USB usb = soundcard.getUSB();
  if(usb != null && "3.0".equals(usb.getVersion()){
    System.out.println("ok");
  }
}
複製代碼

咱們可使用map方法重寫這種檢測null,而後再提取對象類型的對象。

Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
複製代碼

這個和使用stream的map函數式同樣的。使用stream須要給map函數傳遞一個函數做爲參數,這個傳遞進來的函數將會應用於stream中的每一個元素。當stream時空的時候,什麼也不會發生。

Optional中包含的值將會被傳遞進來的函數轉化(這裏是一個從聲卡中獲取USB的函數)。若是Optional對象時空的,那麼什麼也不會發生。

而後,咱們結合map方法和filter方法過濾掉USB的版本號不是3.0的聲卡。

maybeSoundcard.map(Soundcard::getUSB)
      .filter(usb -> "3.0".equals(usb.getVersion())
      .ifPresent(() -> System.out.println("ok"));
複製代碼

這樣咱們的代碼開始變得像有點像開始咱們給出的樣子,沒有了null檢測。

使用flatMap函數傳遞Optional對象

如今已經介紹了一個可使用Optional重構代碼的例子,那麼咱們應該如何使用安全的方式實現下面代碼呢?

String version = computer.getSoundcard().getUSB().getVersion();
複製代碼

注意上面的代碼都是從一個對象中提取另外一個對象,使用map函數能夠實現。在前面的文章中咱們設置了Computer中包含的是一個Optional對象,Soundcard包含的是一個Optional對象,所以咱們能夠這麼重構代碼

String version = computer.map(Computer::getSoundcard)
                  .map(Soundcard::getUSB)
                  .map(USB::getVersion)
                  .orElse("UNKNOWN");
複製代碼

不幸的是,上面的代碼會編譯錯誤,那麼爲何呢?computer變量是Optional類型的,因此它調用map函數是沒有問題的。可是getSoundcard()方法返回的是一個Optional<Soundcard>的對象,返回的是Optional<Optional<Soundcard>>類型的對象,進行了第二次map函數的調用,結果調用getUSB()函數就變成非法的了。下面的圖描述了這種場景:

輸入圖片說明
Optional>

map函數的源碼實現是這樣的:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }
複製代碼

能夠看出map函數還會再調用一次Optional.ofNullable(),從而致使返回Optional<Optional<Soundcard>>

Optional提供了flatMap這個函數,它的設計意圖是當對Optional對象的值進行轉化(就像map操做)而後一個兩級Optional壓縮成一個。下面的圖展現了Optional對象經過調用map和flatMap進行類型轉化的不一樣:

輸入圖片說明
map和flatMap比較

所以咱們能夠這樣寫:

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

複製代碼

第一個flatMap保證了返回的是Optional而不是Optional<Optional>,第二個flatMap實現了一樣的功能從而返回的是 Optional。注意第三次調用了map(),由於getVersion()返回的是一個String對象而不是一個Optional對象。

咱們終於把剛開始使用的嵌套null檢查的醜陋代碼改寫了可讀性高的代碼,也避免了空指針異常的出現的代碼。

總結

在這片文章中咱們採用了Java 8提供的新類java.util.Optional<T>。這個類的初衷不是要取代null引用,而是幫助設計者設計出更好的API,只要讀到函數的簽名就可知道該函數是否接受一個可能存在也可能不存在的值。另外,Optional迫使你去打開Optional,而後處理值是否存在,這就使得你的代碼避免了潛在的空指針異常。

最後

感謝閱讀,有興趣能夠關注微信公衆帳號獲取最新推送文章。

歡迎關注微信公衆帳號
歡迎關注微信公衆帳號
相關文章
相關標籤/搜索