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

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沒有車,就會形成空指針異常。java

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";
}

上述代碼不具有擴展性,同時還犧牲了代碼的可讀性。segmentfault

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類的特定單一實例。
函數

引入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的屬性值時才返回一個錯誤。spa

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);

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");
}

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類中的方法進行了分類和歸納:

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<Integer>對象,而再也不須要記得你在其中封裝了笨拙的 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<T>,對存在或缺失的變量值進行建模。
  3. 你可使用靜態工廠方法Optional.empty、Optional.of以及Optional.ofNullable建立Optional對象。
  4. Optional類支持多種方法,好比map、flatMap、filter,它們在概念上與Stream類中對應的方法十分類似。
  5. 使用Optional會使你更積極地解引用Optional對象,以應對變量值缺失的問題,最終,你能更有效地止代碼中出現不而至的空指針異常。
  6. 使用Optional能幫助你設計更好的API,用戶只須要閱讀方法簽名,就能瞭解該方法是否接受一個Optional類型的值。

資源獲取

  • 公衆號回覆 : Java8 便可獲取《Java 8 in Action》中英文版!

Tips

  • 歡迎收藏和轉發,感謝你的支持!(๑•̀ㅂ•́)و✧
  • 歡迎關注個人公衆號:莊裏程序猿,讀書筆記教程資源第一時間得到!

相關文章
相關標籤/搜索