Java8新特性之Optiona類進階知識
●再說Optional類
上一篇文章概述性地介紹了一下Optional類,可能許多讀者還是無法很好的掌握。筆者一開始接觸這個類的時候也沒有發現其有什麼特別的好處,特別是對於“可以有效地避免空指標異常”這個特點理解得不夠深刻,為什麼這麼說呢?結合一段程式碼,說說筆者當初的疑惑,以及是如何一步一步解決這個疑惑的:
//原始業務程式碼 User user = userService.getUserbyId(0); user.setUserName("小明"); //初次使用Optionnal User user = userService.getUserbyId(0); Optional<User> op = Optional.of(user); if(op.isPresent()){ op.get().setUserName("小明"); }
邏輯很簡單,設定id為0的使用者姓名為“小明”。原始業務程式碼中,確實可能出現空指標異常,因為資料庫中可能不存在id為0的使用者。因此,應該做一個非空判斷。
初次嘗試使用Optional類,發現居然也出現了一個空指標異常,在Optional<User> op = Optional.of(user);這一行,覺得很奇怪,不是說Optional類可以裝一個null物件嗎?不是說Optional類可以有效地避免空指標異常嗎?怎麼還會這樣呢?後來又仔細查了一下,發現原來學到的只是皮毛,如果用工廠方法Optional.of()構造一個Optional類,必須保證引數是一個非空物件。但這個時候依然有疑惑,不是說Optional可以包裝一個null的物件嗎,為什麼還會報空指標異常呢?
順藤摸瓜,我們來看看Optional.of()方法的原始碼:
/** * Returns an {@code Optional} with the specified present non-null value. * * @param <T> the class of the value * @param value the value to be present, which must be non-null * @return an {@code Optional} with the value present * @throws NullPointerException if value is null */ public static <T> Optional<T> of(T value) { return new Optional<>(value); } /** * Constructs an instance with the value present. * * @param value the non-null value to be present * @throws NullPointerException if value is null */ private Optional(T value) { this.value = Objects.requireNonNull(value); } /** * Checks that the specified object reference is not {@code null}. This * method is designed primarily for doing parameter validation in methods * and constructors, as demonstrated below: * <blockquote><pre> * public Foo(Bar bar) { * this.bar = Objects.requireNonNull(bar); * } * </pre></blockquote> * * @param obj the object reference to check for nullity * @param <T> the type of the reference * @return {@code obj} if not {@code null} * @throws NullPointerException if {@code obj} is {@code null} */ public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }
原來,使用這個工廠方法構造Optional類的時候,用到了Objects.requireNonNull()的空值校驗方法,因此如果傳入引數為null,確實會產生空指標異常。結合實際情況,此時應該採用另一個工廠方法Optional.ofNullable()來進行構造。
OK,既然這樣,那我們將程式碼更改為:
User user = userService.getUserbyId(0);
Optional<User> op = Optional.ofNullable(user);
if(op.isPresent()){
op.get().setUserName("小明");
}
可以,問題又來了,確實,你可以說使用.isPresent()是函式呼叫,可是它真的體現了函數語言程式設計的優點嗎?這種寫法和下面的寫法不是類似的嗎?
User user = userService.getUserbyId(0);
if(null != user){
user.setUserName("小明");
}
我如果忘記使用if(op.isPresent())對Optional物件判空,程式照樣報空指標異常。網上有一種解釋,使用Optional強制你每次都要做採用固定的步驟:isPresent()和get(),強制要求你做Optional非空判斷,避免報錯。當時筆者的第一反應是WTF???那我以後每個物件也採用固定的步驟:if(null != T),強制要求你做非空判斷,避免報錯,這不是一樣的嗎?簡直是強行套個解釋嘛。
後來,筆者又閱讀了大量Optional的文章以及《Java8函數語言程式設計》這本書的相關章節,仔細思考後,終於有點領悟,和大家分享一下Optional的正確開啟方式——
- 放棄掉isPresent()和get()的用法,forget it!!!
- 充分結合java8的Lambda與Stream新特性,來一次鏈式呼叫吧。
網上告訴你的isPresent()和get()固定套路,根本就是瞎扯,真正體現Optional“有效避免空指標異常”是其ifPresent()、orElse()、orElseGet()以及orElseThrow()這幾個方法。
業務場景(op代表Optional物件) | 正確用法示例 | 錯誤用法示例 |
如果op中的物件不為空,則進行操作 | op.ifPresent(o -> o.setUserName("小明")); |
if(op.isPresent()){ op.get().setUserName("小明")); } |
如果op中的物件不為空,則返回它;否則返回另一個值 | op.orElse(initUser); |
if(op.isPresent()){ return op.get(); } else{ return initUser; } |
如果op中的物件不為空,則返回它;否則進行操作 | op.orElseGet(() -> new User(0, "小明")); | if(op.isPresent()){
return op.get(); } else{ return new User(0, "小明"); } |
如果op中的物件不為空,則返回它;否則丟擲異常 |
op.orElseThrow(IllegalArgumentException::new); |
if(op.isPresent()){
return op.get(); } else{ throw new IllegalArgumentException() } |
●實戰演練程式碼重構
這麼說比較抽象,我麼結合具體的專案程式碼看一看,例如,前臺發起請求,傳來一個報警引數,後臺提取報警的ID,去資料庫中查詢對應ID的報警事件,並且獲取該報警事件的名字和型別。我們來看看實現這個需求,傳統的JAVA7的程式碼會怎麼寫:
public String test0(AlarmAllParmeter alarmAllParmeter) {
String errorResult = "";
if (null != alarmAllParmeter) {
Integer alarmId = alarmAllParmeter.getAlarmEventInputId();
if (null != alarmId) {
AlarmEventInput alarmEventInput = alarmEventInputService.get(alarmId);
if (null != alarmEventInput) {
String alarmName = alarmEventInput.getAlarmName();
int alarmType = alarmEventInput.getAlarmType();
return String.valueOf(alarmType) + "-" + alarmName;
} else {
return errorResult;
}
} else {
return errorResult;
}
} else {
return errorResult;
}
}
可以明顯看出,為了防止空指標異常,我們在程式碼中寫了大量的if(null != T)的模板程式碼。而初次學習Optional類的朋友很可能會寫出如下程式碼:
public String test1(AlarmAllParmeter alarmAllParmeter){
String errorResult = "";
Optional<AlarmAllParmeter> op = Optional.ofNullable(alarmAllParmeter);
if(op.isPresent()){
Integer alarmId = op.get().getAlarmEventInputId();
Optional<Integer> op1 = Optional.ofNullable(alarmId);
if(op1.isPresent()){
AlarmEventInput alarmEventInput = alarmEventInputService.get(op1.get());
Optional<AlarmEventInput> op2 = Optional.ofNullable(alarmEventInput);
if (op2.isPresent()) {
String alarmName = alarmEventInput.getAlarmName();
int alarmType = alarmEventInput.getAlarmType();
return String.valueOf(alarmType) + "-" + alarmName;
} else {
return errorResult;
}
}
else {
return errorResult;
}
}
else {
return errorResult;
}
}
可以看出,其程式設計的思路還是停留在“指令式程式設計”的層面,這種強行用Optional類的做法反而顯得多此一舉,本質上和傳統寫模板程式碼一樣,真的就只是給物件套了個Optional容器而已。接下來,我們用Optional正確的開啟方式來實現這個需求,重構最初的程式碼:
public String test2(AlarmAllParmeter alarmAllParmeter){
return Optional.ofNullable(alarmAllParmeter)
.map(a -> a.getAlarmEventInputId())
.map(a -> alarmEventInputService.get(a))
.map(a -> String.valueOf(a.getAlarmType())+"-"+a.getAlarmName())
.orElse("");
}
看見了嗎,這就是JAVA8的魅力所在,Optional、Lambda、Stream的綜合應用,極大的簡化了程式碼的書寫(方法體中其實就只要一行程式碼就可以完成,但是為了方便閱讀,強烈建議大家在實戰中按步驟分行完成鏈式呼叫)。這似乎看上去已經不是熟悉的那個Java語言了,但時代在進步,只有接受、習慣並順利上手高效的新技術才能追得上整個行業的潮流。
●小結
關於Java8新特性的文章已經寫了三篇了,基本上可以用到實際的專案中了,無論是開啟新的專案,或者是重構以前臃腫的程式碼,都是值得一試的。筆者經過學習,發現Java8的改變帶給人最大的感受是函數語言程式設計的思想,這和以前指令式程式設計的思想完全不同,短期要完全接受還是有點難度的,但真的習慣了這種新的程式設計思路你會發現效率非常的高。就好比當初學習C++的時候,對比C,感覺多了一個物件的概念,比較抽象,但之後再回頭去看,才真的體會到面向物件程式設計相比於面向過程程式設計的強大優勢所在。不得不說Java8的這顆語法糖真的很甜。
最後,再次強調一下,這一篇文章所提到的Optional的正確使用方式是進行鏈式處理,而不應該像不少網文所說的那樣去做isPresent()判斷,再去get()取值。今天,你學會了嗎?