一文掌握 Java8 的 Optional 的 6 種操作


Java8 中引入了一個特別有意思類:Optional,一個可以讓我們更加輕松的避免 NPE(空指針異常,NullPointException)的工具。

很久很久以前,為了避免 NPE,我們會寫很多類似if (obj != null) {}的代碼,有時候忘記寫,就可能出現 NPE,造成線上故障。在 Java 技術棧中,如果誰的代碼出現了 NPE,有極大的可能會被笑話,這個異常被很多人認為是低級錯誤。Optional的出現,可以讓大家更加輕松的避免因為低級錯誤被嘲諷的概率。 定義示例數據 先定義待操作對象,萬能的Student類和Clazz類(用到了 lombok 和 guava):

@Data@AllArgsConstructor@NoArgsConstructorpublic class Clazz { private String id; private String name;}

@Data@AllArgsConstructor@NoArgsConstructorpublic class Student { private String id; private String name; private Clazz clazz;}

然后定義一組測試數據:

final Clazz clazz1 = new Clazz("1", "高一一班"); final Student s1 = new Student("1", "張三", clazz1);final Student s2 = new Student("2", "李四", null); final List students = Lists.newArrayList(s1, s2);final List emptyStudents = Lists.newArrayList();final List nullStudents = null;

創建實例:of、ofNullable 為了控制生成實例的方式,也是為了收緊空值Optional的定義,Optional將構造函數定義為private。想要創建Optional實例,可以借助of和ofNullable兩個方法實現。

這兩個方法的區別在於:of方法傳入的參數不能是null的,否則會拋出NullPointerException。所以,對於可能是null的結果,一定使用ofNullable。

代碼如下:

Optional.of(students);Optional.of(emptyStudents);Optional.ofNullable(nullStudents);

Optional類中還有一個靜態方法:empty,這個方法直接返回了內部定義的一個常量Optional<?> EMPTY = new Optional<>(),這個常量的value是null。ofNullable方法也是借助了empty實現null的包裝:

public static Optional ofNullable(T value) { return value == null ? empty() : of(value);}

所以說,對於null的Optional包裝類,指向的都是相同的實例對象,Optional.empty() == Optional.ofNullable(null)返回的是true。換句話說,空Optional是單例的。

為了方便描述,下文中對值為null的Optional統稱為“空Optional”。 獲取數據:get Optional的get方法有些坑人,先看下它的源碼:

public T get() { if (value == null) { throw new NoSuchElementException("No value present"); } return value;}

也就是說,Optional值為空時,java培訓使用get方法將拋出NoSuchElementException異常。如果不想拋出異常,或者能夠 100%確定不是空Optional,或者使用isPresent方法判斷。

如果能 100%確定不是空Optional,那就沒有必要使用Optional包裝,直接返回即可。如果需要使用isPresent方法,那就和直接判空沒有區別了。所以,無論是第一種情況還是第二種情況,都違背了設計這個類的初衷。 值為空判斷:isPresent、ifPresent isPresent用來判斷值是否為空,類似於obj != null,ifPresent可以傳入一個Consumer操作,當值不為空的時候,會執行Consumer函數。比如:

final Optional<List> nullValue = Optional.ofNullable(nullStudents); if (nullValue.isPresent()) { System.out.println("value: " + nullValue.get());}

上面的方法等價於:

nullValue.ifPresent(value -> System.out.println("value: " + value));

isPresent判斷的寫法上是不是感覺很熟悉,感覺可以直接寫為:

if (nullStudents != null) { System.out.println("value: " + nullStudents);}

對於isPresent,如果是在自己可控的代碼范圍內,完全沒有必要將值封裝之后再判空。對於自己不可控的代碼,后續的filter或者map方法可能比isPresent更好用一些。

對於ifPresent,在使用的時候會有一些限制,就是必須是非空Optional的時候,在會執行傳入的Consumer函數。 值處理:map、flatMap map和flatMap是對Optional的值進行操作的方法,區別在於,map會將結果包裝到Optional中返回,flatMap不會。但是兩個方法返回值都是Optional類型,這也就要求,flatMap的方法函數返回值需要是Optional類型。

我們來看看map的實現:

public Optional map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); }}

可以看到,如果Optional的值為空,map直接返回Optional.EMPTY,否則會執行函數結果,並使用Optional.ofNullable包裝並返回。也即是說,只要類結構允許,我們可以一直map下去,就像是扒洋蔥,一層一層,直到核心。

比如,我們要獲取s2所在班級名稱,在定義的時候,我們將s2的clazz屬性定義為 null,如果以前需要寫為:

String clazzNameOld;if (s2 != null && s2.getClazz() != null && s2.getClazz().getName() != null) { clazzNameOld = s2.getClazz().getName();} else { clazzNameOld = "DEFAULT_NAME";}

現在借助Optional可以寫為:

final String clazzName = Optional.ofNullable(s2) .map(Student::getClazz) .map(Clazz::getName) .orElse("DEFAULT_NAME");

從代碼上似乎沒有多大改變,但是如果Clazz內部還有類對象。或者,我們在if判斷的時候,少寫一層檢查呢?而且,map的精巧還在於它的返回值永遠是Optional,這樣,我們可以重復調用map方法,而不需要中間被打斷,增加各種判空邏輯。 值為空的處理:orElse、orElseGet、orElseThrow 這幾個方法可以與map操作結合,一起完成對象操作。當值為空時,orElse和orElseGet返回默認值,orElseThrow拋出指定的異常。

orElse和orElseGet的區別是,orElse方法傳入的參數是明確的默認值,orElseGet方法傳入的參數是獲取默認值的函數。如果默認值的構造過程比較復雜,需要經過一系列的運算邏輯,那一定要使用orElseGet,因為orElseGet是在值為空的時候,才會執行函數,並返回默認值,如果值不為空,則不會執行函數,相比於orElse而言,減少了一次構造默認值的過程。

同樣以上面的例子:

orElse的寫法:

final String clazzName = Optional.ofNullable(s2) .map(Student::getClazz) .map(Clazz::getName) .orElse(null);

orElseGet的寫法:

final String clazzName = Optional.of(s2) .map(Student::getClazz) .map(Clazz::getName) .orElseGet(() -> null);

如果clazz屬性一定不為空,為空則返回異常,可以使用orElseThrow:

final String clazzName = Optional.of(s2) .map(Student::getClazz) .map(Clazz::getName) .orElseThrow(() -> new IllegalArgumentException("clazz屬性不合法"));

條件過濾:filter filter方法提供的是值驗證,如果值驗證為 true,返回當前值;否則,返回空Optional。比如,我們要遍歷students,找到班級屬性為空的,打印學生 id:

for (final Student s : students) { Optional.of(s) .filter(x -> x.getClazz() == null) .ifPresent(x -> System.out.println(x.getId()));}

其他:equals、hashCode、toString Optional重寫了這三個方法。因為Optional可以認為是包裝類,所以還是圍繞這被包裝的值重寫這三個方法。下面給出這三個方法的源碼:

public boolean equals(Object obj) { // 同一對象判斷 if (this == obj) { return true; } // 類型判斷 if (!(obj instanceof Optional)) { return false; } Optional other = (Optional) obj; // 最終還是值的判斷 return Objects.equals(value, other.value);} public int hashCode() { // 直接返回值的hashCode return Objects.hashCode(value);} public String toString() { return value != null ? String.format("Optional[%s]", value) // 用到了值的toString結果 : "Optional.empty";}

equals方法,Optional.of(s1).equals(Optional.of(s2))完全等價於s1.equals(s2)。

hashCode方法,直接返回的是值的 hashCode,如果是空Optional,返回的是 0。

toString方法,為了能夠識別是Optional,將打印數據包裝了一下。如果是空Optional,返回的是字符串“Optional.empty”;如果是非空,返回是是“Optional[值的 toString]”。 文末總結 NPE 之所以討厭,就是只要出現 NPE,我們就能夠解決。但是一旦出現,都已經是事后,可能已經出現線上故障。偏偏在 Java 語言中,NPE 又很容易出現。Optional提供了模板方法,有效且高效的避免 NPE。

接下來,我們針對上面的使用,總結一下:

  1. Optional是一個包裝類,且不可變,不可序列化
  2. 沒有公共構造函數,創建需要使用of、ofNullable方法
  3. 空Optional是單例,都是引用Optional.EMPTY
  4. 想要獲取Optional的值,可以使用get、orElse、orElseGet、orElseThrow

另外,還有一些實踐上的建議:

  1. 使用get方法前,必須使用isPresent檢查。但是使用isPresent前,先思考下是否可以使用orElse、orElseGet等方法代替實現。
  2. orElse和orElseGet,優先選擇orElseGet,這個是惰性計算
  3. Optional不要作為參數或者類屬性,可以作為返回值
  4. 盡量將map、filter的函數參數抽出去作為單獨方法,這樣能夠保持鏈式調用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM