到目前為止,我已經使用 Java 8 數個月了,幾個月下來新功能從完全不懂到漸漸熟悉。大致上學習與運用分幾個階段:
- 先開始運用 Stream 和最簡單的 lambda
- 強迫自己不寫任何 for/while,全部用 Stream 解決
- 導入 java.time
- 開始大量使用 method reference
- 嘗試使用 default method
- 開始廣泛運用 Optional
當然實際上沒有完全分這麼細,這麼地按步就班。不過大致上是先熟悉 stream 再熟悉 method reference ,最後是 Optional。
題外話 Method Reference
在研究 lambda 的時候,我一開始認為 method reference 太過魔術,省掉太多的資訊,會造成程式可讀性變低。不過實際用一陣子後,發現它沒那麼複雜,它與 Stream API 搭配使用時,不是接零個參數 (通常是 constructor),就是接單一個參數,而這個參數多半是 stream 中的資料而已。
// 用 method reference 可以少掉了 a, n 這種中間沒意義的變數,
// 也可以減少 ->, () 等等防礙閱讀的符號
// 另外,可使用 method reference 的地方也表示它沒有 capture 任何變數
accounts.stream()
.map(a -> a.getNickname())
.forEach(n -> System.out.println(n));
accounts.stream()
.map(Account::getNickname)
.forEach(System.out::println);
當然要一眼看懂 method reference 在做什麼還是要一點訓練的,不過這門檻並不高。所以我個人的建議是能用就用吧,它現在已經變成我 Java 8 最愛的新功能了!
重新檢視 Optional
好了,回今天的主題,Optional
,它在我的學習歷程中是最後才開始上路,一方面是 Java 本身的 API 多半沒對應,另一方面它也不適合漸進式導入,因為很容易出現新舊風格不一的情況。現在實際用了一陣子,整理了一些心得:
Optional.of() vs. Optional.ofNullable()
使用 Optional 第一個會撞牆的就是這兩個 factory method。在 Java 8 中,of(value)
中的 value 不能接 null,只有 ofNullable(value)
才行。我一開始只用 of()
來建立 Optional,所以執行期噴了一堆錯誤,所以現在就都改採用 ofNullable()
。
我不知道為什麼要設計成這樣,也許是學理上有些原因吧。但我覺得這實在是很差的設計,我查了一下用到 Optional 的程式碼,大概九成的案例都是用 ofNullable()
。為什麼?會用到 Optional 的地方一定是會有 null 啊,不然用它幹嘛?現在 Java8 設計了一個繞舌又難打的 ofNullable method,程式碼難看了許多,改用 from()
或是 resolve()
等名稱都還好些。不管怎樣現在也只能用下去了,這個錯誤的設計值得大家借鏡。
Elvis Operator 替代品
Elvis Operator 在一些語言上有,不過 Java 還沒有:
// Java 只有 ternary if operator
String displayName = getNickname() != null ? getNickname() : "Unknown";
// Groovy 則有 Elvis operator
var displayName = getNickname() ?: "Unknown" ;
Elvis operator 不只是讓程式碼簡潔而已,有時候像是 getNickname()
這樣的 method 是個重度操作 (例如來自資料庫),如果沒 Elvis 就會變成這樣:
// ternary operator 會變 query 兩次,相信大家不會這樣寫:
String displayName = accountDao.findNickname(1223) != null
? accountDao.findNickname(1223)
: "Unknown" ;
// 通常會改寫成這樣:
String displayName = accountDao.findNickname(1223);
if (displayName == null) {
displayName = "Unknown";
}
本來 Elvis operator 一行可以搞定的,變成要四行,而且 displayName
也不能設成 final,這很痛苦。這時 Optional 可以派上用場:
final String displayName = Optional.ofNullable(accountDao.findNickname(1223))
.orElse("Unknown");
有了 Java8 的 Optional,終於可以把這難看的四行程式碼換成一行簡潔的程式碼。這當然跟專屬的 operator 是不能比的,但是好非常多了。
當然,這是最基本的 Optional 的 API 運用,本來是沒什麼好提的。不過,因為運用的時機非常多,所以會成為新的成語 (Programming idiom)。不論你們團隊會導入 Optional 到什麼程度,那些 map()
、filter()
、method reference 你都不太熟,但這個最基本的 Elvis by Optional 成語你都應該熟悉。隨著越來越多的人升級到 Java8,這個成語相信會隨處可見。
Optional Argument
我們有了 Optional,那麼,它適合用在任何可能出現 null 的地方嗎?我們先來看看運用在參數上的例子,試想一下這樣的 API:
//用 email 建立帳號,生日和性別不一定要填
public Account create(String email,
Optional<LocalDate> birthday,
Optional<Gender> gender);
嗯,看起來挺合理的,那麼用的人會…
// (1) 認真讀你的 API 乖乖牌
accountDao.create("foo@gmail.com",
Optional.empty(),
Optional.of(Gender.MALE));
// (2) 一般 Java 開發者
accountDao.create("foo@gmail.com", null, null)
現實中,很少人會認真的像 (1)
的例子一樣,還乖乖的傳 empty() 給你,我想大部份的人都會用 (2)
的方法呼叫。Optional 用在參數上有幾個缺點:
- API 真的不好用啊,每次呼叫,不管有沒有值都要用 Optional 包一次參數。
- 調用 API 的人,不小心會傳 null 給你,然後 compiler 也檢查不出來。他不知道呼叫錯了。
- 由於用的人還是會傳 null 進來,變成實作 API 的人不管怎麼樣也要處理傳入 null 的情況,多一層 Optional 反而變多餘。
在 Java 8 in Action 這本書裡,作者是建議可以這樣設計 API,希望盡可能享有 Optional 帶來的優點。但我個人實際用過後,覺得沒有什麼優點,缺點還一堆,所以我不會這樣設計 public API。參數上採用 Optional,我只會考慮在 private method 上使用。
參數如果可以接受 null,建議按下列的準則:
- 可以 null 的參數盡可能放在後面
- 可考慮標示 @Nullable 的 annotation
- 如果大多數的使用例都是傳 null 居多,可考慮 overloading 新的 method。
- 參數過多的 method,建議改用 Builder pattern。
OptionalReturn ?
OptionalReturn
這啥?原本 Java 8 設計團隊可能會接受用這個名字取代我們現在看到的 Optional。是的,設計團隊希望 Optional 只該用在回傳值上,它根本就不該出現在參數上,也不該出現在 instance field 。因為這樣的決策,所以 Optional 有很多限制,它不俱備 Serializable,所以不能用在 instance field,也不適用在 RPC 上 (RMI)。即然設計成這樣了,現階段我們使用者也只能在這樣的限制使用。
Optional vs. Exception
我在前幾篇文章提到 Optional 的適用範圍,再複述如下:
// Optional 適合用在有查詢、檢驗、解析... 等等會有未知
// 結果涵意的 action method 上,看看下面的例子:
// 例1: 可能查無此人
interface AccountDao {
Optional<Account> findByName(String name);
}
// 例2: 解析地址可能失敗
interface Address {
static Optional<Address> tryParse(String address) {...}
}
// 例3: 檢查名字是否唯一,重覆就不建立
interface AccountService {
Optional<Account> tryCreate(String uniqueName);
}
經過這幾個月的實際操作演練,這些規則仍然適用。把原本會丟 exception 的地方,改成 Optional 後 API 就變好用了。不過,Optional 雖然可以某方面取代 Exception,但不能完全取代:
//當 例3 需要揭露多種錯誤的情況時,Optional 就不夠用了
Account create(String uniqueName)
throws DuplicateNameException, NameTooShortException;
很多時候你需要揭露多種錯誤狀況,你還是需要用 exception。另外,如果邏輯上 無法建立 Account 是不合理的,你也不能用 Optional 取代 exception。
Optional 適用在本來只有單一 exception ,而且沒有值是合理的回傳值上:
interface Address {
// 這個範例只有單純的錯誤,所以適合轉成 Optional,不過 exception
// 的版本仍可以提供有用的錯誤資訊
static Address parse(String rawAddress)
throws AddressFormatException {...}
// 你可以提供丟 exception 的 API,也可以同時提供 Optional 的版本,沒
// 規定只能用一種。當然 Optional 的版本可優先考慮,因為還是比較好用。
static Optional<Address> tryParse(String rawAddress) {...}
// 但不要設計 API 是回傳 Optional ,但也會同時丟 exception,這完全沒意義。
static Optional<Address> bad_example_parse(String rawAddress)
throws AddressFormatException {...}
}
最後,提一下命名規則,如果 method 呼叫後,結果是失敗時,但不會丟 exception,我通常會冠上 try
這個字。這個慣例來自於 concurrent Lock 以及 Spliterator 等 API。我想在 Java API 中已出現不少次使用 try
當做前綴字的例子,大部份的開發者應該可以接受這樣的慣例。我建議大家採用。
使用 Optional 後,不會再遇到 NPE ?
NPE是指... 喔,我不能說出它的全名,那只有鄧不利多能說。經過幾個月的 Optional 實驗,結論是我還是遇到了 NPE。我遇到的案例是這樣的:
Account tryValidate(String accessToken) {
if (someLogicCheck(accessToken)) {
if (anontherLogicCheck(accessToken, httpSession)) {
return null;
}
}
//...
//很長的運算... 十幾行...
//...
return new Account(...);
}
//test case
@Test
public void testInvalidAccessToken() {
assertNull(validator.tryValidate("a bad access token"));
}
這個 method 解析 accessToken ,成功的話回傳 Account 物件。後來我把這個 method 改成 Optional,過程是:
// 第一步先改 signature
Optional<Account> tryValidate(String accessToken) {
//...
// 第二步,IDE 會抱怨回傳 Account 是錯的,所以就包起來吧
return Optional.of(new Account(...));
}
程式改好,測試也過了,接著就是上線。然後 validator.tryValidate(someInput).map(...)
這地方就爆 NPE 了。你應該已經看出我犯了什麼錯誤,還看不出來的話那表示 Optional 真的很危險。
也許打一開始撰寫 method 時就回傳 Optional,我就不會犯錯了,不過改程式和重構是日常作業啊!即使我寫了單元測試還是沒測到 (assertNull 那行實在太不起眼了)。經過這次的教訓,我認為即使全部 method 換用 Optional 也不能完全解決 NPE。只能說能降低機會吧。會這樣根本的原因還是 Java compiler 不會檢查 Optional type 不能吃 null。
如果只針對這種直接 return null 造成的錯誤,現在是有解的。FindBugs 上個月出爐最新版 3.0 了,它可以抓到這種錯誤。
當 Optional 遇見 Object
除了 NPE 外,Optional 實際使用還有很多地雷,每個我都踩到了,請大家跟著我也來踩一遍:
//承上例,tryValidate 回傳的是 Optional<Account>
//例1. validate 結果放到 HttpSession
public void session(String token) {
httpSession.setAttribute("account", validator.tryValidate(token));
//其他地方拿出 Account... 砰!
Account exist = (Account) httpSession.getAttribute("account");
}
//例2. validate 結果放到 Spring web 的 ModelAndView
public ModelAndView renderView(String token) {
modelAndView.addObject("account", validator.tryValidate(token));
return modelAndView;
}
//然後在 html view 中顯示... 砰!
<span >${account.nickname}</span>
//例3. validate 結果從 List 中移除
private List<Account> loginedAccounts = ...;
public void logout(String token) {
//logout 後從登入的列表中移除... 砰!
loginedAccounts.remove(validator.tryValidate(token));
}
砰砰砰,連三爆。我現在講的輕鬆,但這些地雷有時候花了幾個小時才找到,真的很想砸電腦。它們都有共同的特徵,ModelAndView、HttpSession、List.remove() 的參數都是接 Object
型別,所以你傳進 Optional 時,都無消無息的石沉大海,直到執行期才發現問題。你可以想像我將 tryValidate()
修改成 Optional 後,這些吃 Object 參數的地方全都變成未爆彈。
這三個地雷,List.remove()
FindBugs 抓得到型別錯誤。而 HttpSession
FindBugs 則會警告 Optional 不是 Serializable,不該放入 session 中。不過 ModelAndView
它就幫不上忙了,越冷門的 API FindBugs 越使不上力。
話說回來,我真的認為 Java 8 的 Optional 根本不該繼承 Object 啊。而且這樣設計的話,它自然也不能吃 null 了。不過這樣的改變太大了,不可能在 Java 8 發生,也許 Java 9, 10 之後引進 value type 之後,會有改進的實作。
總結
小小的 Optional,看看 Java 8 的原始碼,它還不到 100 行,值得這麼大書特書嗎?我也希望它很簡單,不過實務上使用它時會有很多需要衡量的地方:
- Optional 可擔任 Java Elvis Operator 的角色
- Optional 不適合用在參數上
- Optional 不支援 Serializable,不適合放在 instance field
- Optional 的本職是用在回傳值,適用有未知結果涵意的 action method 上
- Optional 不可完全取代 Exception,它擔任互補的角色
- Optional 不能完全解決 NPE,你仍然需要小心,以及搭配 FindBugs 輔助
- Optional 搭配萬用型別的容器使用容易出錯。
- OptionalInt 等變型應避免使用,因為他們缺乏 map/flatMap 這類 method。
本文完全沒討論 Optional 它的 map/flatMap 那些 Monad 的實例應用,因為相對上面總結那幾條,Monad 真是簡單多了啊~ (被毆