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
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。