厭倦了空指針異常?考慮使用Java SE 8的Optional!

  • 2020 年 3 月 16 日
  • 筆記

使您的代碼更可讀,並保護它免受空指針異常。

—————–來自小馬哥的故事


說明

一個聰明的人曾經表示,在處理空指針異常之前,你不是一個真正的Java程序員。開玩笑,空引用是許多問題的根源,因為它通常用於表示沒有值。Java SE 8引入了一個新的類java.util.Optional,可以減輕其中的一些問題。

我們從一個例子開始,看到null的危險。我們來看一個嵌套的對象結構Computer,如圖1所示。

圖1:用於表示a的嵌套結構 Computer

以下代碼可能有問題嗎?

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

這段代碼看起來很合理。然而,許多計算機(例如,Raspberry Pi)實際上並不附帶聲卡。那麼結果是getSoundcard()什麼呢?

一個常見的(bad)做法是返回null引用以指示沒有聲卡。不幸的是,這意味着調用getUSB()將嘗試返回一個空引用的USB端口,這將導致NullPointerException運行時,並阻止程序進一步運行。想像一下,如果您的程序在客戶的機器上運行; 如果程序突然失敗,您的客戶會說什麼? 為了給出一些歷史背景,計算機科學巨人托尼·霍爾(Tony Hoare)寫道:「我稱之為我十億美元的錯誤,這是1965年發明的無效參考。我無法抗拒放棄的誘惑一個null引用,只是因為它很容易實現。「

你可以做什麼來防止意外的空指針異常?您可以防禦並添加檢查以防止取消引用,如下列代碼所示:

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

但是,由於嵌套檢查,您可以看到清單1中的代碼很難變得非常難看。不幸的是,我們需要很多樣板代碼,以確保我們沒有得到NullPointerException。此外,這些檢查妨礙了業務邏輯,這是令人討厭的。實際上,它們正在減少我們的程序的整體可讀性。

此外,這是一個容易出錯的過程; 如果你忘記檢查一個屬性可能是null怎麼辦?我將在本文中討論使用null表示缺少值是錯誤的方法。我們需要的是更好地模擬一個價值的缺失和存在。

為了給出一些上下文,我們來簡要介紹一下其他的編程語言。

沒有什麼替代品?

諸如Groovy之類的語言具有由「 」 表示的安全導航操作,?.用於安全瀏覽潛在的空引用。(請注意,它很快被包含在C#中,並且被提出用於Java SE 7,但沒有將其納入該版本。)它的工作原理如下: 諸如Groovy之類的語言具有由「 」 表示的安全導航操作,?.用於安全瀏覽潛在的空引用。(請注意,它很快被包含在C#中,並且被提出用於Java SE 7,但沒有將其納入該版本。)它的工作原理如下:

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

在這種情況下,變量version將被分配為null,如果computer為null,或getSoundcard()返回null,或getUSB()返回null。您不需要編寫複雜的嵌套條件來檢查null。

此外,Groovy還包括Elvis操作員 「 ?:」(如果您側身看着,您會認識到Elvis着名的頭髮),當需要默認值時,可以使用它。在下列情況下,如果使用安全導航運算符的表達式返回null,"UNKNOWN"則返回默認值; 否則返回可用的版本標籤。

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

其他功能語言,如Haskell和Scala,採取不同的視圖。Haskell包括一個Maybe類型,它基本上封裝了一個可選的值。類型Maybe的值可以包含給定類型的值或不包含任何值。沒有空引用的概念。Scala有一個類似的結構,Option[T]用於封裝類型值的存在或不存在T。然後,您必須使用Option類型上可用的操作來顯式檢查值是否存在,這強加了「空檢」的想法。你不能再「忘記這樣做」,因為它是由類型系統執行的。

好的,我們分歧了一切,這聽起來很抽象。您可能現在想知道,「那麼Java SE 8呢?」

Optional 簡而言之

Java SE 8引入了一個名為j的新類ava.util.Optional,它來自Haskell和Scala的想法。它是一個封裝可選值的類,如下面的清單2和圖1所示。您可以將其Optional視為包含值或不包含值的單值容器(它被稱為「空」) ,如圖2所示。

我們可以更新我們的模型以使用Optional public class Computer { private Optional soundcard; public Optional getSoundcard() { … } … }

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

代碼立即顯示計算機可能有也可能沒有聲卡(聲卡是可選的)。此外,聲卡可以選擇具有USB端口。這是一個改進,因為這個新模型現在可以清楚地反映給定值是否被允許丟失。請注意,類似的想法已經在圖書館,如番石榴。

但是你可以用一個Optional對象來做什麼呢?畢竟,你想要獲得USB端口的版本號。簡而言之,Optional該類包括明確處理值存在或不存在的情況的方法。然而,與空引用相比的優點是,Optional當該值不存在時,該類迫使您考慮該情況。因此,您可以防止意外的空指針異常。

重要的是要注意,Optional類的意圖不是替換每個單個空引用。相反,其目的是幫助設計更易於理解的API,以便通過讀取方法的簽名,您可以判斷是否可以期望可選的值。這迫使你主動打開一個Optional處理沒有價值的東西。

採用模式 Optional

夠說話 讓我們看看一些代碼!我們將首先探討如何使用更改典型的空檢查模式Optional。在本文結尾,您將了解如何使用Optional,如下所示,重寫清單1中正在進行多個嵌套空值檢查的代碼:

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

注意:確保刷新Java SE 8 lambdas和方法引用語法(請參閱「 Java 8:Lambdas 」)及其流流水線概念(請參閱「 使用Java SE 8 Streams處理數據 」)。

創建Optional對象

首先,你如何創建Optional對象?有幾種方法:

這是一個空的Optional:

Optional<Soundcard> sc = Optional.empty(); 

這裡是Optional一個非空值:

SoundCard soundcard = new Soundcard();  Optional<Soundcard> sc = Optional.of(soundcard); 

如果soundcard為null,NullPointerException則會立即拋出一個(而不是在嘗試訪問該屬性時發生潛在錯誤soundcard)。

另外,通過使用ofNullable,您可以創建一個Optional可能保持空值的對象:

Optional<Soundcard> sc = Optional.ofNullable(soundcard); 

如果Soundcard為空,則生成的Optional對象將為空。

做某事如果價值存在

現在你有一個Optional對象,你可以訪問可用的方法來明確地處理值的存在或不存在。而不必記得做一個空檢查,如下所示:

SoundCard soundcard = ...;  if(soundcard != null){    System.out.println(soundcard);  }

您可以使用以下ifPresent()方法:

Optional<Soundcard> soundcard = ...;  soundcard.ifPresent(System.out::println);

您不再需要執行明確的空檢查; 它由類型系統執行。如果Optional對象為空,則不會打印任何內容。

您還可以使用該isPresent()方法來確定Optional對象中是否存在值。另外還有一個get()方法返回Optional對象中包含的值,如果它存在的話。否則,它會拋出一個NoSuchElementException。這兩種方法可以組合起來,如下,以防止異常:

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

然而,這不是推薦使用Optional(對嵌套空檢查來說,這不是很大的改進),而且有更多的慣用選擇,我們在下面探討。

默認值和操作

典型的模式是返回默認值,如果確定操作的結果為空。一般來說,您可以使用三元運算符來實現:

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);

使用filter方法拒絕某些值

通常,您需要調用對象上的方法並檢查某些屬性。例如,您可能需要檢查USB端口是否是特定版本。要以安全的方式執行此操作,您首先需要檢查指向USB對象的引用是否為空,然後調用該getVersion()方法,如下所示:

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

可以使用對象filter上的方法重寫此模式Optional,如下所示:

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

該filter方法使用謂詞作為參數。如果一個值存在於Optional對象中,並與謂詞匹配,則該filter方法返回該值; 否則返回一個空Optional對象。如果您已經使用filter該Stream接口的方法,您可能已經看到了類似的模式。

使用該map方法提取和轉換值

另一種常見的模式是從對象中提取信息。例如,從Soundcard對象中,您可能需要提取USB對象,然後進一步檢查它是否是正確的版本。你通常會寫下面的代碼:

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

我們可以Soundcard使用該map方法重寫「檢查null和提取」(這裡是對象)的這種模式。

Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);

map與流一起使用的方法是直接平行的。在那裡,您將一個函數傳遞給map方法,該方法將此函數應用於流的每個元素。但是,如果流為空,則不會發生任何事情。

該類的map方法Optional完全相同:內部包含的值Optional通過作為參數傳遞的函數進行「轉換」(這裡是提取USB端口的方法引用),而如果Optional為空,則不會發生任何反應。

最後,我們可以將map方法與filter方法結合使用,以拒絕其版本不同於3.0的USB端口:

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

真棒; 我們的代碼開始看起來更接近於問題陳述,並且沒有詳細的null檢查方式!

Optional使用flatMap方法級聯對象

您已經看到可以重構使用的幾種模式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");

不幸的是,這段代碼沒有編譯。為什麼?可變計算機是類型Optional,所以調用該map方法是完全正確的。但是,getSoundcard()返回一個類型的對象Optional。這意味着地圖操作的結果是類型的對象Optional<Optional>。結果,調用getUSB()是無效的,因為最外層Optional包含其值Optional,當然不支持該getUSB()方法。圖3說明了Optional您將獲得的嵌套結構。

那麼我們如何解決這個問題呢?再次,我們可以看一下以前使用stream的方式:flatMap方法。使用流,該flatMap方法將一個函數作為參數,返回另一個流。該功能應用於流的每個元素,這將導致流的流。然而,flatMap具有通過該流的內容替換每個生成的流的效果。換句話說,由函數生成的所有單獨的流被合併或「扁平化」成一個流。我們在這裡想要的是類似的東西,但是我們希望將兩層平鋪Optional成一層。

好的,這是個好消息:Optional也支持一種flatMap方法。其目的是將變換函數應用於一個值Optional(就像地圖操作那樣),然後將所得到的兩個層次平坦Optional化為一個。圖4示出之間的差map和flatMap在變換函數返回一個Optional對象。

圖4:使用map與flatMap用Optional

所以,為了使我們的代碼正確,我們需要重寫如下使用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對象。

哇!我們從編寫痛苦的嵌套空白檢查到編寫能夠組合,可讀和更好地保護空指針異常的聲明性代碼已經走了很長的路。

結論

在本文中,我們已經看到了如何採用新的Java SE 8 java.util.Optional。目的Optional不是替換代碼庫中的每一個空引用,而是幫助設計更好的API – 只要讀取方法的簽名,用戶就可以判斷是否期望可選的值。另外,Optional迫使你主動展開一個Optional處理沒有價值的東西; 因此,您可以保護您的代碼免受意外的空指針異常。

Optional類使用場景

Optional類應該作為可能有返回值函數的返回值類型。有人甚至建議Optional類應該改名為OptionalReturn。 Optional類不是為了避免所有的空指針類型機制。方法或構造函數輸入參數強制性檢查就仍然是有必要的。 在以下場景一般不建議使用Optional類。

  • 領域模型層(非序列化)
  • 數據傳輸對象(同上原因)
  • 方法的輸入參數
  • 構造函數參數

Optional類方法參考

下面摘抄Optional類的方法,供參考

序號

方法

描述

1

static Optional empty()

返回空的可選實例。

2

boolean equals(Object,obj)

指示是否一些其他的對象是「等於」這個選項。

3

Optional filter(Predicate<? super predicate)

如果某個值存在,且該值與給定的謂詞匹配,則它返回一個可選的描述值,否則返回一個空的可選值。

4

Optional flatMap(Function<? super T,Optional> mapper)

如果存在一個值,它將提供的可選軸承映射函數應用到它,返回結果,否則返回空可選。

5

T get()

如果一個值是可選的,返回值,否則拋出NoSuchElementException。

6

int hashCode()

返回當前值的哈希代碼值(如果有的話),如果沒有值,則返回0(0)。

7

void ifPresent(Consumer<? super T> consumer)

如果存在一個值,它用值調用指定的消費者,否則什麼也不做。

8

boolean isPresent()

如果有一個價值存在返回true,否則為false。

9

Optional map(Function<? super T,? extends U> mapper)

如果存在一個值,則將所提供的映射函數應用於它,如果結果為非null,則返回一個可選的描述結果。

10

static Optional of(T value)

返回一個可選的指定非空值。

11

static Optional ofNullable(T value)

返回一個可選的描述指定值,如果非NULL,否則返回一個空可選。

12

T orElse(T other)

如果目前的返回值,否則返回其他。

13

T orElseGet(Supplier<? extends T> other)

返回當前的值,否則調用其他,並返回該調用的結果。

14

T orElseThrow(Supplier<? extends X> exceptionSupplier)

返回所包含的值,如果存在,則拋出由所提供的供應商創建的異常。

15

String toString()

返回此選項的非空字符串表示,適合調試。

本文由 小馬哥 創作,採用 知識共享署名4.0 國際許可協議進行許可 本站文章除註明轉載/出處外,均為本站原創或翻譯,轉載前請務必署名 最後編輯時間為: 2017/11/23 09:19