天天看點

Effective Java 第三版——55. 明智而審慎地傳回Optional

Tips

書中的源代碼位址:https://github.com/jbloch/effective-java-3e-source-code

注意,書中的有些代碼裡方法是基于Java 9 API中的,是以JDK 最好下載下傳 JDK 9以上的版本。

55. 明智而審慎地傳回Optional

在Java 8之前,編寫在特定情況下無法傳回任何值的方法時,可以采用兩種方法。要麼抛出異常,要麼傳回null(假設傳回類型是對象是引用類型)。但這兩種方法都不完美。應該為異常條件保留異常(條目 69),并且抛出異常代價很高,因為在建立異常時捕獲整個堆棧跟蹤。傳回null沒有這些缺點,但是它有自己的缺陷。如果方法傳回null,用戶端必須包含特殊情況代碼來處理null傳回的可能性,除非程式員能夠證明null傳回是不可能的。如果用戶端忽略檢查null傳回并将null傳回值存儲在某個資料結構中,那麼會在将來的某個時間在與這個問題不相關的代碼位置上,抛出

NullPointerException

異常的可能性。

在Java 8中,還有第三種方法來編寫可能無法傳回任何值的方法。

Optional<T>

類表示一個不可變的容器,它可以包含一個非null的

T

引用,也可以什麼都不包含。不包含任何内容的Optional被稱為空(empty)。非空的包含值稱的Optional被稱為存在(present)。Optional的本質上是一個不可變的集合,最多可以容納一個元素。

Optional<T>

沒有實作

Collection<T>

接口,但原則上是可以。

在概念上傳回T的方法,但在某些情況下可能無法這樣做,可以聲明為傳回一個

Optional<T>

。這允許該方法傳回一個空結果,以表明不能傳回有效的結果。傳回Optional的方法比抛出異常的方法更靈活、更容易使用,而且比傳回null的方法更不容易出錯。

在條目 30中,我們展示了根據集合中元素的自然順序計算集合最大值的方法。

// Returns maximum value in collection - throws exception if empty
public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (c.isEmpty())
        throw new IllegalArgumentException("Empty collection");

    E result = null;
    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
    return result;
}
           

如果給定集合為空,此方法将抛出

IllegalArgumentException

異常。我們在條目30中提到,更好的替代方法是傳回

Optional<E>

。下面是修改後的方法:

// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>>
        Optional<E> max(Collection<E> c) {
    if (c.isEmpty())
        return Optional.empty();
 
    E result = null;
    for (E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
    return Optional.of(result);
}
           

如你所見,傳回Optional很簡單。 你所要做的就是使用适當的靜态工廠建立Optional。 在這個程式中,我們使用兩個:

Optional.empty()

傳回一個空的Optional,

Optional.of(value)

傳回一個包含給定非null值的Optional。 将null傳遞給

Optional.of(value)

是一個程式設計錯誤。 如果這樣做,該方法通過抛出

NullPointerException

異常作為回應。

Optional.of(value)

方法接受一個可能為null的值,如果傳入null則傳回一個空的Optional。永遠不要通過傳回Optional的方法傳回一個空值:它破壞Optional設計的初衷。

Stream

上的很多終止操作傳回Optional。如果我們重寫max方法來使用一個

Stream

,那麼

Stream

max

操作會為我們生成Optional的工作(盡管我們還是傳遞一個顯式的

Comparator

):

// Returns max val in collection as Optional<E> - uses stream
public static <E extends Comparable<E>>
        Optional<E> max(Collection<E> c) {
    return c.stream().max(Comparator.naturalOrder());
}
           

那麼,如何選擇傳回Optional而不是傳回null或抛出異常呢?

Optional

在本質上類似于檢查異常(checked exceptions)(條目 71),因為它們迫使API的使用者面對可能沒有傳回任何值的事實。抛出未檢查的異常或傳回null允許使用者忽略這種可能性,進而帶來潛在的可怕後果。但是,抛出一個檢查異常需要在用戶端中添加額外的樣闆代碼。

如果方法傳回一個Optional,則用戶端可以選擇在方法無法傳回值時要采取的操作。 可以指定預設值:

// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");
           

或者可以抛出任何适當的異常。注意,我們傳遞的是異常工廠,而不是實際的異常。這避免了建立異常的開銷,除非它真的實際被抛出:

// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
           

如果你能證明Optional非空,你可以從Optional擷取值,而不需要指定一個操作來執行。但是如果Optional是空的,你判斷錯了,代碼會抛出一個

NoSuchElementException

異常:

// Using optional when you know there’s a return value
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
           

有時候,可能會遇到這樣一種情況:擷取預設值的代價很高,除非必要,否則希望避免這種代價。對于這些情況,Optional提供了一個方法,該方法接受

Supplier<T>

,并僅在必要時調用它。這個方法被稱為

orElseGet

,但是或許應該被稱為

orElseCompute

,因為它與以

compute

開頭的三個Map方法密切相關。有幾個Optional的方法來處理更特殊的用例:

filter

map

flatMap

ifPresent

。在Java 9中,又添加了兩個這樣的方法:

or

ifPresentOrElse

。如果上面描述的基本方法與你的用例不太比對,請檢視這些更進階方法的文檔,并檢視它們是否能夠完成任務。

如果這些方法都不能滿足你的需要,Optional提供

isPresent()

方法,可以将其視為安全閥。如果Optional包含值,則傳回true;如果為空,則傳回false。你可以使用此方法對可選結果執行任何喜歡的處理,但請確定明智地使用它。

isPresent

的許多用途都可以被上面提到的一種方法所替代。生成的代碼通常更短、更清晰、更符合習慣。

例如,請考慮此代碼段,它列印一個程序的父程序ID,如果程序沒有父程序,則列印N/A. 該代碼段使用Java 9中引入的

ProcessHandle

類:

Optional<ProcessHandle> parentProcess = ph.parent();
System.out.println("Parent PID: " + (parentProcess.isPresent() ?
    String.valueOf(parentProcess.get().pid()) : "N/A"));
           

上面的代碼可以被如下代碼所替代,使用了Optional的

map

方法:

System.out.println("Parent PID: " +

  ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
           

當使用Stream進行程式設計時,通常會發現使用的是一個

Stream<Optional<T>>

,并且需要一個

Stream<T>

,其中包含非Optional中的所有元素,以便繼續進行。如果你正在使用Java 8,下面是彌補這個差距的代碼:

streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)
           

在Java 9中,Optional配備了一個

stream()

方法。這個方法是一個擴充卡, 此方法是一個擴充卡,它将Optional變為包含一個元素的Stream,如果Optional為空,則不包含任何元素。此方法與Stream的

flatMap

方法(條目45)相結合,這個方法可以簡潔地替代上面的方法:

streamOfOptionals.
    .flatMap(Optional::stream)
           

并不是所有的傳回類型都能從Optional的進行中獲益。容器類型,包括集合、映射、Stream、數組和Optional,不應該封裝在Optional中。與其傳回一個空的

Optional<List<T>>

,不還如傳回一個空的

List<T>

(條目 54)。傳回空容器将消除用戶端代碼處理Optional的需要。

ProcessHandle

類确實有

arguments

方法,它傳回

Optional<String[]>

,但是這個方法應該被視為一種異常,不該被效仿。

那麼什麼時候應該聲明一個方法來傳回

Optional <T>

而不是

T

呢? 通常,如果可能無法傳回結果,并且在沒有傳回結果,用戶端還必須執行特殊處理的情況下,則應聲明傳回Optional 的方法。也就是說,傳回

Optional <T>

并非沒有成本。 Optional是必須配置設定和初始化的對象,從Optional中讀取值需要額外的迂回。 這使得Optional不适合在某些性能關鍵的情況下使用。 特定方法是否屬于此類别隻能通過仔細測量來确定(條目 67)。

與傳回裝箱的基本類型相比,傳回包含已裝箱基本類型的Optional的代價高得驚人,因為Optional有兩個裝箱級别,而不是零。是以,類庫設計人員認為為基本類型int、long和double提供類似Option是合适的。這些Option是

OptionalInt

OptionalLong

OptionalDouble

。它們包含

Optional<T>

上的大多數方法,但不是所有方法。是以,除了“次要基本類型(minor primitive types)”Boolean,Byte,Character,Short和Float之外,永遠不應該傳回裝箱的基本類型的Optional。

到目前為止,我們已經讨論了傳回Optional并在傳回後處理它們的方法。我們還沒有讨論其他可能的用法,這是因為大多數其他Optional的用法都是可疑的。例如,永遠不要将Optional用作映射值。如果這樣做,則有兩種方法可以表示鍵(key)在映射中邏輯上的缺失:鍵要麼不在映射中,要麼存在的話映射到一個空的Optional。這反映了不必要的複雜性,很有可能導緻混淆和錯誤。更通俗地說,在集合或數組中使用Optional的鍵、值或元素幾乎都是不合适的。

這裡留下了一個懸而未決的大問題。在執行個體中存儲Optional屬性是否合适嗎?通常這是一種“不好的味道”:它建議你可能應該有一個包含Optional屬性的子類。但有時這可能是合理的。考慮條目2中的

NutritionFacts

類的情況。

NutritionFacts

執行個體包含許多不需要的屬性。不可能為這些屬性的每個可能組合都提供一個子類。此外,屬性包含基本類型,這使得很難直接表示這種缺失。對于

NutritionFacts

最好的API将為每個Optional屬性從getter方法傳回一個Optional,是以将這些Optional作為屬性存儲在對象中是很有意義的。

總之,如果發現自己編寫的方法不能總是傳回值,并且認為該方法的使用者在每次調用時考慮這種可能性很重要,那麼或許應該傳回一個Optional的方法。但是,應該意識到,傳回Optional會帶來實際的性能後果;對于性能關鍵的方法,最好傳回null或抛出異常。最後,除了作為傳回值之外,不應該在任何其他地方中使用Optional。