【Guava 教學】(1)從避免使用 null 開始


大多數開發者對 Guava 的認識,是從其中對Java 標準 API 中 Java Collections 框架功能的增強或補充開始,那是 guava-libraries 中的一部份,也就是 Guava Collections,在這部份,Java Collections 框架參與者之一 Joshua Bloch 也曾對其提出相關建議。然而 guava-libraries 不僅僅是 Collections 的功能加強版,它還包括了許多進階特性,甚至有不少設計是借鏡了函數式程式設計的概念,像是不可變動結構、避免 null 等,對不少 Java 傳統開發者而言,這些概念都是新穎而陌生的,也因此無法對其作更進一步的善用。
在 guava-libraries 的 Wiki 上,一開始就談到了 Using/avoiding null,上頭一開始引用了 Java Collections 群集框架及 JSR166 參與者之一 Doug Lea 的話:

"Null sucks." 

接著又引用了圖靈獎得主、快速排序發明者 Tony Hoare,在 QCon London 2009 主講《Null References: The Billion Dollar Mistake》場次時的一段話:

"I call it my billion-dollar mistake."

null 的問題在於含糊而不明確,引發的各種問題從Java 開發者經常在與 NullPointerException 奮戰可見一般,我在 補救 null 的策略 中談過:

null 的最根本問題在於語意含糊不清,雖然就字面來說,null 可以是「不存在」、「沒有」、「無」或「空」的概念,因此在應用時,總是令人感到模稜兩可,也就讓開發者有了各自解釋的空間,當開發者想到「嘿!這邊可以沒有東西...」就直接放個 null,或者是想到「嗯!沒什麼東西可以傳回...」,就不假思索地傳回個 null,然後使用者就總是忘了檢查 null,引發各種可能的錯誤。

由於 null 的根本問題在於含糊而不明確,要避免使用 null 的方式,就是確認過去使用 null 的時機與目的,並使用明確的語義。在過去使用 null 的情況中,開發者於方法中傳回 null,通常代表著客戶端必須檢查是否為 null,並在 null 的情況下使用預設值,以便後續程式繼續執行。舉個例子來說,如果原先有個 getNickName 方法可傳回 String,而客戶端有以下的執行流程:
String nickName = getNickName("Duke");
if(nickName == null) {
    nickName = "CodeData User";
}
out.println(nickName);
如果客戶端忘了檢查 null,那麼就會直接顯示 null,在這個簡單的例子中並不會怎樣,只是顯示結果令人困惑罷了,但如果後續的執行流程牽涉到至關重要的結果,程式快樂地繼續執行下去,錯誤可能到最後才會呈現發生。
那麼可將 getNickName 修改使一定會傳回 Optional<String>(在 com.google.common.base 套件)實例,但絕不要傳回 nullOptional 的語義是它可能包含也可能不包括值,如果你呼叫直接呼叫它的 get 方法:
String nickName = getNickName("Duke").get();
out.println(nickName);
Optional 沒有包含值的情況下,就會直接拋出 IllegalStateException,這實現了速錯(Fail fast)的概念,這讓開發者可以立即發現錯誤,並瞭解到必須作些檢查,可能的方式之一像是:
Optional<String> nick = getNickName("Duke");
String nickName = nick.isPresent() ? nick.get() : "CodeData User";
out.println(nickName);
不過這看來有點囉嗦,一個比較好的方式可以是:
String nickName = getNickName("Duke").or("CodeData User");
out.println(nickName);
getNickName 方法內部,原先如果是這樣傳回值:
return rs.next() ? rs.getString("nickname") : null;
則可以使用 Optional 改為:
return rs.next() ? Optional.of(rs.getString("nickname")) : Optional.absent();
Optional.of 方法用來建立 Optional 物件包裏 of 的傳入值,而 Optional.absent 建立的 Optional 實例不會包裹任何值,也就是如果你直接呼叫後者建立的 Optional 物件上 get 方法,就會拋出例外。在有值的情況下使用 Optional.of,在原本會傳回 null 的情況下使用 Optional.absent
當然,過去許多程式庫中使用了不少 null,這些程式庫無法說改就改,Guava 提供了一些銜接程式庫中 null 的方法。例如,如果原先的 getNickName 是你無法修改的,那麼可以這麼修改客戶端:
String nickName = Optional.fromNullable(getNickName("Duke")).or("CoDeData User");
out.println(nickName);
Optional.fromNullable 在傳入值為 null 的情況下,傳回的 Optional 實例呼叫其 get 方法,就會拋出錯誤。 在 Guava 上還有一些方法,是針對字串而設定,最主要就是讓語義清楚。因為過去對於字串沒有值,各開發者會有各自的表示,因而造成同一個程式中,有可能某些開發者認為字串沒有值時,應該傳回 null,有的開發者則認為空字串才是表示字串沒有值。無論如何,在需要統一的場合中,可以使用 Strings.emptyToNullStrings 是在 com.google.common.base 套件)、Strings.nullToEmpty 方法。如果程式中想統一字串沒有值時,必須是 null,那麼可在原本採用空字串的地方呼叫 Strings.emptyToNull(""),反之,若想統一字串沒有值時一律以空字串表示,那麼就使用 Strings.nullToEmpty(null)
現在,你可以進一步看看 Guava 的 Wiki 上 Using/avoiding null 說的是怎麼一回事了。所以你知道嗎?JDK8 中也會有個 Optional