沒有什麼比 Optional 更好,真的,沒有,更好
許多部落格宣稱 Optional 類別解決空指標的問題,假的
Translated from “Nothing is better than the Optional type. Really. Nothing is better”, Michael Ernst, Java Magazine December 9, 2022. Copyright Oracle Corporation.
許多部落格宣稱 Optional
類別解決空指標的問題,假的
JDK 導入 Optional 類別,一個可能是空的或是包含一非空值 (non-null value) 的容器。
Optional 有許多問題,且沒有相應的好處,它不會讓您的程式更正確或強健。Optional 試圖解決一個真實的問題,本文展示一種更好的解決方式。因此,您最好使用一個常規且可能是 null 的 Java 參考,而不是 Optional。
許多網路與部落格充宣稱 Optional 類別解決空指標引起例外的問題,這並非是真的。將您的程式碼改用 Optional 類別會帶來以下負面效果:
- Optional 將
NullPointerException
轉成NoSuchElementException
,仍造成程式閃退 (Crash)。 - Optional 產生一個之前不存在的問題
- Optional 讓程式凌亂
- Optional 增加空間、時間與程式的開銷。
當您的程式拋出 NullPointerException
或 NoSuchElementException
時,潛在的邏輯錯誤是在處理資料時您忘記檢查所有的可能性,最好是有工具確保您不會忘記 [譯註:個人認為編譯器是最適合的工具],有助於讓您知道並修正潛在的問題。
這些批評不限於 Java 的 Optional 實作,其他聲稱解決空指標問題的語言,也僅僅是將其轉化成其他不同型式。
釐清一點,Optional 並非全是糟的,若您需要使用 Optional 處理可能存在的資料時,它提供函式減少程式的雜亂,然而,您仍然應該避免使用 Optional 類別。
本文其他部分解釋上述提到的觀點。
改變拋出的例外並沒有修正缺陷或改善程式
考慮以下的笛卡爾座標的點類別,有 x 與 y 欄位 (這討論同樣適用於使用 getter 函式):
class Point { int x; int y; }
因為 Java 參考可能是空的 (null),當您試圖存取,比如以下的 myPoint.x
,有拋出 NullPointerException
的風險,
Point myPoint;
...
... myPoint.x ...
如果 myPoint
是空的,則 myPoint.x
拋出 NullPointerException
造成程式閃退,以下是使用 Optional
的一種方法:
Point myPoint;
Optional<Point> myOPoint = Optional.ofNullable(myPoint);
...
... myOPoint.get().x ...
如果 myOPoint
沒有包含一個真的點,則 myOPoint.get().x
拋出 NoSuchElementException
例外,然後程式閃退,這沒有比原本的程式碼好任何一丁點,因為程式設計師的目標是避免所有可能的閃退,不僅僅是 NullPointerException
造成的閃退。
要避免例外與閃退,可以先進行檢查
if (myPoint != null) {
... myPoint.x ...
}
或
if (myOPoint.isPresent()) {
... myOPoint().get().x ...
}
同樣,程式碼非常相似,Optional 並沒有比使用常規的 Java 參考高明。
Optional 容易被誤用
Optional 是一個 Java 類別,因此,型別是 Optional<Point>
的變數 myOPoint
本身也可能是空的,所以 myOPoint.get()
可能拋出 NullPointerException
或 NoSuchElementException
,您需要寫這樣的程式碼:
if (myOPoint != null && myOPoint.isPresent()) {
... myOPoint.get().x ...
}
您可透過空的 Optional、沒有數值的非空 Optional 以及有數值的非空 Optional 表達複雜的資料結構,但這變得很複雜與讓人困惑。或著,您可以選擇放棄這些可能性,並謹慎且有紀律地避免 Optional 型別的值是空的。然而,如果您對此有足夠的信心,一開始便不會有空指標的例外,您也不需要使用 Optional。
Optional 是個包裝器,因此,對於值很敏感的操作都是容易出錯的,包含檢查參考的相等 (==
)、雜湊語同步操作,您要記住不能使用這些操作。
這不是新鮮事,早在 2016 年,Stuart Marks 提供了一長串的規則避免錯誤地使用 Optional。
Optional 讓程式凌亂
使用有 Optional 的函式庫,您的程式碼變得更囉嗦,如下例子所示:
- 型別宣告:
Optional<Point>
之於Point
- 檢查是否有值:
myOPoint.isPresent()
之於myPoint == null
- 存取值:
myOPoint.get().x
之於myPoint.x
這些單獨都不致命,但總體來說,Optional 使用上非常麻煩且不美觀,想要看具體的例子,可以看 Code Project 部落格 “Why we should love ‘null’”,並搜尋 cumbersome 關鍵字。
Optional 增加開銷
Optional 增加空間開銷:一個 Optional 是個獨立物件,占用額外的記憶體。
Optional 增加時間開銷:資料須透過額外的間接存取,呼叫函式比 Java 對空值的測試昂貴 [譯註:JVM 有特別優化]。
Optional 增加開發的開銷:您必須處理既有使用 null 的介面與 Optional 不相容的問題,以及它無法序列化等等 [譯註:這一點真的很討厭,特別是處理 JSON 的時候]。
真正的問題:記得要檢查
會拋出 NullPointerException
或 NoSuchElementException
是因為程式設計師在使用資料前,忘記用 != null
或是 .isPresent()
檢查資料是否存在。
許多人說 Optional 的好處是,有了 Optional,您不太可能忘記進行檢查,如果是真的,那太好了。然而,這不足以讓問題在少數使用 Optional 的地方較少發生,更好的作法是有個能在每個地方消除問題的保證。
一種方式是強制程式設計師總是在存取資料前進行檢查 (這是部分語言的做法,提供建構子或樣式比對),結果是在許多已檢查或是不需檢查的地方進行重複的檢查 (作為類比,想想部分程式設計師對於不論想或是不想都得處理 checked exception 的反應)
較好的作法是有工具能確保您不會忘記檢查,但又不用重複的檢查,幸運地是,此類工具是存在的,像是 Checker Framework 的 Nullness Checker、NullAway 以及 Infer 等。(註:我是 Checker Framework 的創建者)
以 Nullness Checker 為例,它在編譯期檢查您程式中每個存取的操作,並確保接收者知道可能是非空值,這是因為您已經檢查過或是它來自一個永不產生空值的來源。
Nullness Checker 利用強大的分析持續追蹤一個參考是否可能為空值,與 Optional 相較,這減少警告的次數以及所需要的重複檢查數量,Nullness Checker 預設所有的參考都是非空的,但您可以使用 @Nullable
標註可能缺少的資料,例如 @Nullable Point
。
使用 @Nullable Point
相似於 Optional<Point>
,但有以下顯著優點:
- 減少凌亂,因為
@Nullable
是標示在欄位或是函式的宣告 (signature) 上,通常不會在函式主體中。 - 與既有的 Java 程式與函式庫相容,不需要因為呼叫 Optional 的函式而改變您的程式,不需要為了使用 Optional 改變介面或是客戶端,也不用在 Optional 實例與一般的參考之間轉換。
- 沒有運行期間的開銷
- 編譯時可以獲得保證或是警告,不會是運行時的閃退。
- 程式碼被更好地文件化,如果一個型別上沒有 Optional,您不知道是否是程式設計師忘記了,Optional 可能為了向下相容而沒有使用,或是該資料永遠都存在,有 Nullness Checker 靜態分析,編譯期檢查註釋 (annotation),所以每個可能是空值的參考都被標上
@Nullable
。 - 您得到關於空指標例外來源的保證,像是部分初始化的物件或是呼叫
Map.get
,這是 Optional 無法適用的。它亦可用來表達函式的前提條件,對於欄位可能缺少某些資料是非常有用的。
Nullness Checker 實現了確保您絕對不會在程式的任何地方忘記檢查資料是否存在的目標,相較 Optional 更少干擾,其他工具像是 NullAway 和 Infer 在不同的取捨下,提供相似的保證。
由於與空參考 (null reference) 相關的錯誤在使用 Optional 時一樣可能會發生,且 Optional 可能產生新錯誤,程式設計師需要支援以避免所有這類的錯誤,Checker Framework 框架包含一個編譯期間的 Optional Checker,可以完全支援,如果您需要使用 Optional (例如串接有使用 Optional 的函式庫),它非常有用。
Optional 的糖衣函式
雖然 Optional 往往會使您的程式凌亂,如果您需要使用 Optional,它提供一些函式減少這些凌亂,這有兩個例子:
對於一般的 Java 參考,也有類似功能的函式庫,像是 Checker Framework 附帶的 Opt class,對於 Optional 的每個實例函式,Opt 類別都有對應的靜態函式。
Optional API 文件中的其他函式,像是 filter 或是 flatMap,消除大部分需要呼叫 Optional.isPresent()
及 Optional.get()
的需求,這是一大優點,但是他們無法消除全部的需求,且其他 Optional 的缺點仍在。
反駁論點
並非所有人都宣稱 Optional 解決空指標例外的問題,例如,Oracle 的 JDK 團隊並沒有這樣宣稱。
一個程式設計的通則是盡可能避免資料不存在的情況,如此便能減少需要使用 Optional<Point>
或 @Nullable Point
的情況,您的程式中若資料可能不存在,本文的所有論點都成立。
某些人建議 (參見 Stuart Marks 的演講),程式設計師應該少用 Optional,例如只用在函式的回傳值,不該用在欄位上。如果您少用 Optional,程式碼會少點凌亂。然而,想減少 Optional 帶來的問題,唯一方法是不要使用它。此外,如果您少用 Optional,亦少得到它帶來的好處。無論來源或是語法形式,空指標例外是如此重要,因此,最佳的解法是處理您程式中每個參考,而不是只處理其中一部分。
回傳值使用 Optional 的主要論述是:客戶太容易忘記處理回傳值是空的情況,Optional 很醜 [譯註:我覺得是作者故意用 ugly 這個字],就在您面前,所以客戶不太可能忘記要處理回傳值是空的情況,其他地方,程式設計師應該繼續使用一般的參考,儘管可能是 null。相反,我的建議是,程式設計師應該在他們的程式中,每個地方都繼續使用一般的參考,然後使用工具確保在每個可能的地方,不僅僅是呼叫函式,任何有需要的地方進行 null 檢查。
如果您找到一種使用 Optional 的風格,在可接受的成本下解決您的問題,那很好,使用它。
結論
使用 Java 的參考比用像是 Optional 之類的特殊函式庫要好,原因如下:
- 處理可能是空的參考時,任何您可能犯的錯誤,在使用 Optional 時,都可能遇到對應的錯誤,其後果 (運行期間的例外) 同樣嚴重,因此,單單使用 Optional 沒有解決任何問題。
- Optional 產生新的潛在問題。
- Optional 的語法比可能為空的參考還醜。
- Optional 比空參考還要沒效率。
- 使用 Optional 的動機是絕對不要忘記檢查,雖然 Optional 無法保證這一點,但其他工具可以,且那些工具更強大且精準,並提供編譯期間的保證。
作者感謝 Stuart Marks 在本文草稿上的評論。
深入探討
譯者的告白
這篇文章的標題我覺得有雙關的味道,“Nothing is better than the Optional type. Really. Nothing is better.”,第一個 Nothing 有點暗示 Optional 並沒有比其他設計好到哪去,第二個 Nothing 是暗示 null 比 Optional 好,也讓我很猶豫到底要不要翻譯標題。至於把 “This is not true” 翻譯成假的,是故意在玩梗。
這些年大量使用 Swift,再加上一點點 Kotlin 的經驗,其實真的覺得,null 這件事應該從語言層次下手,會比用函式庫解決問題漂亮許多,即便是本文介紹的框架提供像是 @NonNull
或是 @Nullable
這類的 Annotation,我都不覺得比 var point: Point?
來的漂亮,或許 Java 之後的版本可以直接考慮借鏡,讓 Java 寫起來不要這麼囉嗦 (我個人是還好,習慣了,但囉嗦確實是讓不少人離開 Java 擁抱其他語言的主要原因)。
最後附上,2014 年,當時使用 Swift 和 Java Optional 的一些想法。
沒想到第 82 篇文章仍然是翻譯文章,閒談系列仍然難產中,不過最近慢慢把閒談系列搬到方格子平台,之後會不會同步到 Medium 再看看吧!