使用Optional擺脫NPE的折磨

javaadu發表於2019-07-16

architectural-architectural-design-architecture

在目前的工作中,我對Java中的Stream和Lambda表示式都使用得很多,之前也寫了兩篇文章來總結對應的知識。

不過對於Optional這個特性,一直沒有很好地使用起來,所以最近又開始閱讀《Java 8實戰》這本書,本文是針對其中第10章的一個學習總結。

背景

在Java中,如果你嘗試對null做函式呼叫,就會引發NullPointerException(NPE),NPE是Java程式開發中的最典型的異常,對於Java開發者來說,無論你是初出茅廬的新人和還工作多年的老司機,NPE經常讓他們翻車。為了避免NPE,他們會加很多if判斷語句,使得程式碼的可讀性變得很差。

從軟體設計的角度來看,null本身是沒有意義的語義,這是一種對缺失變數值的錯誤的建模。

從Java型別系統的角度看,null可以被賦值給任何型別的變數,並且不斷被傳遞,知道最後誰也不知道它是從哪裡引入的。

Optional的引入

Java設計者從Haskell和Scala中獲取靈感,在Java 8中引入了一個新的類java.util.Optional<T>。如果一個介面返回Optional,可以表示一個人可能有車也可能沒有車,這個比簡單的返回Car要更明確,閱讀程式碼的人不需要提前準備業務知識。

Optional的目的就在於此:通過型別系統讓你的領域模型中隱藏的知識顯式地體現在你的程式碼中。

Optional的使用

方法 描述
empty 返回一個空的Optional例項
filter 如果值存在並且滿足提供的過濾條件,則返回包含該值的Optional物件;否則就返回一個空的Optional物件
map 如果值存在,就對該值執行提供的mapping函式呼叫
flatMap 如果值存在,就對該值執行提供的mapping函式呼叫,返回一個Optional型別的值,否則就返回一個空的Optional物件
ifPresent 如果值存在,就執行使用該值的方法呼叫,否則什麼也不做
of 將指定值用Optional封裝之後返回,如果該值為null,則丟擲一個NPE
ofNullable 將指定值用Optional封裝之後返回,如果該值為null,則返回一個空的Optional物件
orElse 如果有值則返回,否則返回一個預設值
orElseGet 如果有值則返回,否則返回一個由指定的Supplier介面生成的值(如果預設值的生成代價比較高的話,則適合使用orElseGet方法)
orElseThrow 如果有值則返回,否則返回一個由指定的Supplier介面丟擲的異常
get 如果值存在,則返回該值,否則丟擲一個NoSuchElementException異常
isPresent 如果值存在則返回true,否則返回false

上面這張表裡列舉了Optional的基礎API,我這裡列舉了一些使用的tips:

  • 你可以用ofNullable將一個可能為null的物件封裝為Optional物件,然後獲取值的時候使用orElse方法提供預設值;可以使用empty方法建立一個空的Optional物件;of方法一般不用,不過如果你知道某個值不可能為null,則可以用Optional封裝該值,這樣它一旦為null就會丟擲異常。
//empty方法的使用
Optional<Car> optCar = Optional.empty();

//of方法的使用
Optional<Car> optCar = Optional.of(car);

//ofNullable方法的使用
Optional<Car> optCar = Optional.ofNullable(car);
  • 你可以使用map方法從Optional物件中它封裝的值中的某個欄位的值;
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName); 
  • 如果需要連續、層層遞進的從某個物件鏈的末端獲取欄位的值,則不能全部使用map方法,需要先使用flatMap,最後再使用map方法;
//轉換之前
public String getCarInsuranceName(Person person) {
  return person.getCar().getInsurance().getName();
}

//轉換後
public String getCarInsuranceName(Optional<Person> person) {
  return person.flatMap(Person::getCar)
               .flatMap(Car::Insurance)
               .map(Insurance::getName)
               .orElse("Unknown");
}
  • Optional中的map、flatMap和filter方法,在概念是與Stream中對應的方法都很類似,區別就在於Optional中的元素至多有一個,算是Stream的一種特殊情況——一種特殊的集合。
  • 不要使用ifPresent和get方法,它們本質上和不適用Optional物件之前的模式相同,都是臃腫的if-then-else判斷語句;
  • 由於Optional無法序列化,所以在領域模型中,無法將某個欄位定義為Optional的,原因是:Optional的設計初衷僅僅是要支援能返回Optional物件的語法,如果我們希望在域模型中引入Optional,則可以用下面這種替代的方法:
public class Person {
  private Car car;
  public Optional<Car> getCarAsOptional() {
    return Optional.ofNullable(car);
  }
}
  • 不要使用基礎型別的Optional物件,原因是:基礎型別的Optional物件不支援map、flatMap和filter方法,而這些方法是Optional中非常強大的方法。

實戰案例

案例1:使用工具類方法改良可能丟擲異常的API

Java方法處理異常結果的方式有兩種:返回null(或錯誤碼);丟擲異常,例如:Integer.parseInt(String)這個方法——如果無法解析到對應的整型,該方法就丟擲一個NumberFormationException,這種情況下我們一般會使用try/catch語句處理異常情況。

一般我們建議將try/catch塊單獨提取到一個方法中,在這裡使用Optional設計這個方法,程式碼如下。在開發中,可以嘗試構建一個OptionalUtility工具類,將這些複雜的try/catch邏輯封裝起來。

public static Optional<Integer> stringToInt(String a) {
  try{
    return Optional.of(Integer.parseInt(s));
  } catch  (NumberFormationException e) {
    return Optional.empty();
  }
}

案例2:綜合案例

現在有個方法,是嘗試從一個屬性對映中獲取某個關鍵詞對應的值,例子程式碼如下:

   public static int readDuration(Properties properties, String name) {
        String value = properties.getProperty(name);
        if (value != null) {
            try {
                int i = Integer.parseInt(value);
                if (i > 0) {
                    return i;
                }
            } catch (NumberFormatException e) {

            }
        }
        return 0;
    }

使用Optional的寫法後,程式碼如下所示:

    public static int readDurationWithOptional(Properties properties, String name) {
        return Optional.ofNullable(properties.getProperty(name))
            .flatMap(OptionalUtility::stringToInt)
            .filter(integer -> integer > 0)
            .orElse(0);
    }

如果需要訪問的屬性值不存在,Properites.getProperty(String)方法的返回值就是一個null,使用noNullable工廠方法就可以將該值轉換為Optional物件;接下來,可以使用flatMap將一個Optional轉換為Optional物件;最後使用filter過濾掉負數,然後就可以使用orElse獲取屬性值,如果拿不到則返回預設值0。

總結

使用Optional的思路和Stream相同,都是鏈式思路,跟資料庫查詢似的,表達力很強,而且省去了哪些複雜的try/catch和if-then-else方法。在後面的開發中,可以使用Optional設計API,這樣可以設計出更安全的介面和方法。


本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。javaadu

相關文章