1. 程式人生 > >《Java 8 in Action》Chapter 10:用Optional取代null

《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沒有車,就會造成空指標異常。

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

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

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

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型別的值。

資源獲取

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

Tips

  • 歡迎收藏和轉發,感謝你的支援!(๑•̀ㅂ•́)و✧
  • 歡迎關注我的公眾號:莊裡程式猿,讀書筆記教程資源第一時間獲得!

相關推薦

Java 8 in Action》Chapter 10Optional取代null

1965年,英國一位名為Tony Hoare的電腦科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的型別語言之一。Hoare選擇null引用這種方式,“只是因為這種方法實現起來非常容易”。雖然他的設計初衷就是要“通過編譯器的自動檢測機制,確保所有使用引用的地方都

java 8(三)--Optional取代null

一、引言先假設有三個類,Student, Bag, Book:class Student{ private Bag bag; public Student(Bag bag) {

optional取代null

per 缺陷 調用 bsp 被調用 except 條件 清晰 封裝   Java8引入了java.util.Optional<T>,它是一個封裝的Optional值的類。變量存在時,Optional類只是對類簡單封裝。變量不存在時,缺失的值會被建模成一個空的Op

《Java8實戰》-第十章筆記(Optional取代null

用Optional取代null 如果你作為Java程式設計師曾經遭遇過NullPointerException,請舉起手。如果這是你最常遭遇的異常,請繼續舉手。非常可惜,這個時刻,我們無法看到對方,但是我相信很多人的手這個時刻是舉著的。我們還猜想你可能也有這樣的想法:“毫無疑問,我承認,對任何一位Java程

Java 8 in Action》Chapter 6流收集資料

1. 收集器簡介 collect() 接收一個型別為 Collector 的引數,這個引數決定了如何把流中的元素聚合到其它資料結構中。Collectors 類包含了大量常用收集器的工廠方法,toList() 和 toSet() 就是其中最常見的兩個,除了它們還有很多收集器,用來對資料進行對複雜的轉換。 指令式

Java 8 in Action》Chapter 1為什麼要關心Java 8

自1998年 JDK 1.0(Java 1.0) 釋出以來,Java 已經受到了學生、專案經理和程式設計師等一大批活躍使用者的歡迎。這一語言極富活力,不斷被用在大大小小的專案裡。從 Java 1.1(1997年) 一直到 Java 7(2011年),Java 通過增加新功能,不斷得到良好的升級。Java 8

Java 8 in Action》Chapter 3Lambda表示式

1. Lambda簡介 可以把Lambda表示式理解為簡潔地表示可傳遞的匿名函式的一種方式:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有一個可以丟擲的異常列表。 匿名——我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多! 函式——我們說它是函式,是因為Lambda函式不像方

Java 8 in Action》Chapter 4引入流

1. 流簡介 流是Java API的新成員,它允許你以宣告性方式處理資料集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷資料集的高階迭代器。此外,流還可以透明地並行處理。讓我們來看一個例項返回低熱量(<400)的菜餚名稱: Java7版本: List<Dish&

Java 8 in Action》Chapter 7並行資料處理與效能

在Java 7之前,並行處理資料集合非常麻煩。第一,你得明確地把包含資料的資料結構分成若干子部分。第二,你要給每個子部分分配一個獨立的執行緒。第三,你需要在恰當的時候對它們進行同步來避免不希望出現的競爭條件,等待所有執行緒完成,最後把這些部分結果合併起來。Java 7引入了一個叫作分支/合併的框架,讓這些操

Java 8 in Action》Chapter 8重構、測試和除錯

我們會介紹幾種方法,幫助你重構程式碼,以適配使用Lambda表示式,讓你的程式碼具備更好的可讀性和靈活性。除此之外,我們還會討論目前比較流行的幾種面向物件的設計模式, 包括策略模式、模板方法模式、觀察者模式、責任鏈模式,以及工廠模式,在結合Lambda表示式之後變得更簡潔的情況。最後,我們會介紹如何測試和除

Java 8 in Action》Chapter 9預設方法

傳統上,Java程式的介面是將相關方法按照約定組合到一起的方式。實現介面的類必須為介面中定義的每個方法提供一個實現,或者從父類中

Java 8 in Action》Chapter 11CompletableFuture組合式非同步程式設計

某個網站的資料來自Facebook、Twitter和Google,這就需要網站與網際網路上的多個Web服務通訊。可是,你並不希望因為等待某些服務的響應,阻塞應用程式的執行,浪費數十億寶貴的CPU時鐘週期。比如,不要因為等待Facebook的資料,暫停對來自Twitter的資料處理。 第7章中介紹的分支/合

Java 8新特性之Optional取代null

NullPointerException,大家應該都見過。這是Tony Hoare在設計ALGOL W語言時提出的null引用的想法,他的設計初衷是想通過編譯器的自動檢測機制,確保所有使用引用的地方都是絕對安全的。很多年後,他對自己曾經做過的這個決定而後悔不已,把它稱為“我價值百萬的重大失誤”。它帶來的後果就

堪比 Java 程式碼除錯的方案 WebStorm + JetBrains IDE Support 外掛,在 WebStorm 內除錯程式碼

準備工作 WebStorm 的 Javascript Debug 只能選擇 chrome(至少在我的電腦上是這樣),所以,JetBrains IDE Support 外掛需要安裝到 chrome 上。 chrome 最新版下載 安裝外掛,在應用中心,搜尋:JetBrai

Java多執行緒10同步不具有繼承性

父類的同步操作子類是不可以繼承獲得的 package unit2; public class Demo8_tongbubujujichengxing { public static void m

Java 8實戰(Java 8 in action)學習總結(三)

Streams API可以表達複雜的資料處理查詢。常用的流操作如下表: 你可以使用filter、distinct、skip和limit對流做篩選和切片。 你可以使用map和flatMap提取或轉換流中的元素。 你可以使用findFirst和findAny方法查詢流中的元素。你可以allMatch、none

Ask HN: Have you moved beyond Java 8 in production?

Have you moved to Java 9 , 10 or 11 in production ? Yes / No ? How big is your source code ? Is it a legacy code ? Does it have automated tests ? How much

Java經典演算法集——如下1、2、2、3、4、5這六個數字,java寫一個main函式,打印出所有不同的排列,如512234、412345等,要求"4"不能在第三位,"3"與"5"不能相連。

轉:http://www.blogjava.net/SongJunke/articles/101741.html 演算法程式題:     該公司筆試題就1個,要求在10分鐘內作完。     題目如下:用1、2、2、3、4、5這六個數字,用java寫一個main函式,打印出所

java 8 Lambda 表示式你會了嗎?

jdk1.8沒有新增新的關鍵字 lambda ,而是用()->{} 這麼一個表示式符號來表示 lambda 這麼一個新鮮的函式是為了什麼?使用 這個表示式 有可以給我們帶來什麼樣的好處呢?敬請關注 本臺 Lambda 的無窮大 在寫之前肯定是要看關於

Java web學習總結10HttpServletRequest物件1

一、HttpServletRequest介紹   HttpServletRequest物件代表客戶端的請求,當客戶端通過HTTP協議訪問伺服器時,HTTP請求頭中的所有資訊都封裝在這個物件中,通過這個物件提供的方法,可以獲得客戶端請求的所有資訊。 二、Request常用方法