《Java 8 in Action》Chapter 10:用Optional取代null

莊裡程式猿發表於2019-04-01

1965年,英國一位名為Tony Hoare的電腦科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的型別語言之一。Hoare選擇null引用這種方式,“只是因為這種方法實現起來非常容易”。雖然他的設計初衷就是要“通過編譯器的自動檢測機制,確保所有使用引用的地方都是絕對安全的”,他還是決定為null引用開個綠燈,因為他認為這是為“不存在的值”建模最容易的方式。很多年後,他開始為自己曾經做過這樣的決定而後悔不已,把它稱為“我價值百萬的重大事物”。實際上,Hoare的這段話低估了過去五十年來數百萬程式設計師為修復空引用所耗費的代價。近十年出現的大多數現代程式設計語言1,包括Java,都採用了同樣的設計方式,其原因是為了與更老的語言保持相容,或者就像Hoare曾經陳述的那樣,“僅僅是因為這樣實現起來更加容易”。

1. 如何為確實的值建模

public class Person {
        private Car car;
        public Car getCar() { return car; }
    }
public class Car {
        private Insurance insurance;
        public Insurance getInsurance() { return insurance; }
}
public class Insurance {
    private String name;
    public String getName() { return name; }
}
public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}
複製程式碼

上面這段程式碼的問題就在於,如果person沒有車,就會造成空指標異常。

1.1 採用防禦式檢查減少NullPointerException

1.1.1 深層質疑

簡單來說就是在需要的地方新增null檢查

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
            return insurance.getName();
        }
    }
    return "Unknown";
}
複製程式碼

上述程式碼不具備擴充套件性,同時還犧牲了程式碼的可讀性。

1.1.2 過多的退出語句

public String getCarInsuranceName(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}
複製程式碼

這種模式中方法的退出點有四處,使得程式碼的維護異常艱難。

1.2 null帶來的種種問題

  • 它是錯誤之源。 NullPointerException是目前Java程式開發中最典型的異常。它會使你的程式碼膨脹。
  • 它讓你的程式碼充斥著深度巢狀的null檢查,程式碼的可讀性糟糕透頂。
  • 它自身是毫無意義的。 null自身沒有任何的語義,尤其是是它代表的是在靜態型別語言中以一種錯誤的方式對缺失變數值的建模。
  • 它破壞了Java的哲學。 Java一直試圖避免讓程式設計師意識到指標的存在,唯一的例外是:null指標。
  • 它在Java的型別系統上開了個口子。 null並不屬於任何型別,這意味著它可以被賦值給任意引用型別的變數。這會導致問題, 原因是當這個變數被傳遞到系統中的另一個部分後,你將無法獲知這個null變數最初賦值到底是什麼型別。

1.3 其他語言中null的替代品

  • Groovy中的安全導航操作符
  • Haskell中的Maybe型別
  • Scala中的Option[T]

2. Optional類入門

變數存在時,Optional類只是對類簡單封裝。變數不存在時,缺失的值會被建模成一個“空”的Optional物件,由方法Optional.empty()返回。Optional.empty()方法是一個靜態工廠方法,它返回Optional類的特定單一例項。

《Java 8 in Action》Chapter 10:用Optional取代null

引入Optional類的意圖並非要消除每一個null引用,相反的是,它的目標是幫助開發者更好地設計出普適的API。

3. 應用Optional的幾種模式

3.1 建立Optional物件

3.1.1 宣告一個空的Optional

正如前文已經提到,你可以通過靜態工廠方法Optional.empty,建立一個空的Optional物件:

Optional<Car> optCar = Optional.empty();
複製程式碼

3.1.2 依據一個非空值建立Optional

你還可以使用靜態工廠方法Optional.of,依據一個非空值建立一個Optional物件:

Optional<Car> optCar = Optional.of(car);
複製程式碼

如果car是一個null,這段程式碼會立即丟擲一個NullPointerException,而不是等到你試圖訪問car的屬性值時才返回一個錯誤。

3.2.3 可接受null的Optional

最後,使用靜態工廠方法Optional.ofNullable,你可以建立一個允許null值的Optional物件:

Optional<Car> optCar = Optional.ofNullable(car);
複製程式碼

如果car是null,那麼得到的Optional物件就是個空物件。

3.2 使用map從Optional物件中提取和轉換值

從物件中提取資訊是一種比較常見的模式。

String name = null;
    if(insurance != null){
        name = insurance.getName();
    }
為了支援這種模式,Optional提供了一個map方法。
Optional<Insurance> optInsurance = Optional.ofNullable(insurance); 
Optional<String> name = optInsurance.map(Insurance::getName);
複製程式碼

《Java 8 in Action》Chapter 10:用Optional取代null

3.3 使用flatMap連結Optional物件

使用流時,flatMap方法接受一個函式作為引數,這個函式的返回值是另一個流。 這個方法會應用到流中的每一個元素,最終形成一個新的流的流。但是flagMap會用流的內容替換每個新生成的流。換句話說,由方法生成的各個流會被合併或者扁平化為一個單一的流。

public String getCarInsuranceName(Optional<Person> person) { return person.flatMap(Person::getCar)
                     .flatMap(Car::getInsurance)
                     .map(Insurance::getName)
                     .orElse("Unknown");
}
複製程式碼

《Java 8 in Action》Chapter 10:用Optional取代null

3.4 預設行為及解引用Optional物件

  1. get()是這些方法中最簡單但又最不安全的方法。如果變數存在,它直接返回封裝的變數值,否則就丟擲一個NoSuchElementException異常。所以,除非你非常確定Optional變數一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對於巢狀式的null檢查,也並未體現出多大的改進。
  2. orElse(T other)是我們在程式碼清單10-5中使用的方法,正如之前提到的,它允許你在 Optional物件不包含值時提供一個預設值。
  3. orElseGet(Supplier<? extends T> other)是orElse方法的延遲呼叫版,Supplier 方法只有在Optional物件不含值時才執行呼叫。如果建立預設值是件耗時費力的工作,你應該考慮採用這種方式(藉此提升程式的效能),或者你需要非常確定某個方法僅在 Optional為空時才進行呼叫,也可以考慮該方式(這種情況有嚴格的限制條件)。
  4. orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似,它們遭遇Optional物件為空時都會丟擲一個異常,但是使用orElseThrow你可以定製?希望丟擲的異常型別。
  5. ifPresent(Consumer<? super T>)讓你能在變數值存在時執行一個作為引數傳入的方法,否則就不進行任何操作。

3.5 兩個Optional物件的組合

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    if (person.isPresent() && car.isPresent()) {
        return Optional.of(findCheapestInsurance(person.get(), car.get())); 
    } else {
        return Optional.empty();
    }
}
複製程式碼

3.6 使用filter剔除特定的值

filter方法接受一個謂詞作為引數。如果Optional物件的值存在,並且它符合謂詞的條件, filter方法就返回其值;否則它就返回一個空的Optional物件。

Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
       System.out.println("ok”);
}
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
                        "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));
複製程式碼

Optional類中的方法進行了分類和概括:

《Java 8 in Action》Chapter 10:用Optional取代null
《Java 8 in Action》Chapter 10:用Optional取代null

4. 使用Optional的實戰示例

4.1 用Optional封裝可能為null的值

Optional<Object> value = Optional.ofNullable(map.get("key"));
複製程式碼

每次你希望安全地對潛在為null的物件進行轉換,將其替換為Optional物件時,都可以考慮使用這種方法。

4.2 異常與Optional的對比

public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}
複製程式碼

我們的建議是,你可以將多個類似的方法封裝到一個工具類中,讓我們稱之為OptionalUtility。通過這種方式,你以後就能直接呼叫OptionalUtility.stringToInt方法,將String轉換為一個Optional物件,而不再需要記得你在其中封裝了笨拙的 try/catch的邏輯了。

4.3 把所有內容結合起來

public int readDuration(Properties props, String name) {
    String value = props.getProperty(name);
    if (value != null) {
        try {
            int i = Integer.parseInt(value);
            if (i > 0) {
                return i;
            }
        } catch (NumberFormatException nfe) { }
    }
    return 0; 
}
// 優化版本
public int readDuration(Properties props, String name) {
        return Optional.ofNullable(props.getProperty(name))
                        .flatMap(OptionalUtility::stringToInt)
                        .filter(i -> i > 0)
                        .orElse(0);
}
複製程式碼

5. 小結

這一章中,你學到了以下的內容。

  1. null引用在上被引入到程式設計語言中,目的是為了表示變數值的。
  2. Java 8中引入了一個新的類java.util.Optional,對存在或缺失的變數值進行建模。
  3. 你可以使用靜態工廠方法Optional.empty、Optional.of以及Optional.ofNullable建立Optional物件。
  4. Optional類支援多種方法,比如map、flatMap、filter,它們在概念上與Stream類中對應的方法十分相似。
  5. 使用Optional會使你更積極地解引用Optional物件,以應對變數值缺失的問題,最終,你能更有效地止程式碼中出現不而至的空指標異常。
  6. 使用Optional能幫助你設計更好的API,使用者只需要閱讀方法簽名,就能瞭解該方法是否接受一個Optional型別的值。

Tips

本文同步發表在公眾號,歡迎大家關注!? 後續筆記歡迎關注獲取第一時間更新!

《Java 8 in Action》Chapter 10:用Optional取代null

相關文章