java 8 Optional

Optional

本章內(nèi)容

  • 如何為缺失的值建模
  • Optional 類
  • 應用Optional的幾種模式
  • 使用Optional的實戰(zhàn)實例
  • 小結

如何為缺失的值建模

exp:

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

但是現(xiàn)實生活中很多人沒有車。所以調(diào)用getCar方法的結果會怎樣呢?在實踐中,一種比較常見的做法是返回一個null引用,表示該值的缺失,即用戶沒有車。而接下來,對getInsurance的調(diào)用會返回null引用的insurance,這會導致運行時出現(xiàn)
一個NullPointerException,終止程序的運行。但這還不是全部。如果返回的person值為null會怎樣?如果getInsurance的返回值也是null,結果又會怎樣?
采用防御式檢查減少 NullPointerException,exp:

public String getInstanceName1(Person person) {
    if(person != null) {
        Car car = person.getCar();
        if(car != null) {
            Insurance insurance = car.getInsurance();
            if(insurance != null) {
                return insurance.getName();
            }
        }
    }
    return DEFAULT_INSTANCE_NAME;
}

上面代碼清單為“深層質(zhì)疑”,原因是它不斷重復著一種模式:每次你不確定一
個變量是否為null時,都需要添加一個進一步嵌套的if塊,也增加了代碼縮進的層數(shù)。很明顯,這種方式不具備擴展性,同時還犧牲了代碼的可讀性。

解決這種嵌套過深可以使用衛(wèi)語句來解決,exp:

public String getInstanceName2(Person person) {
    if(person == null) {
        return DEFAULT_INSTANCE_NAME;
    }
    Car car = person.getCar();
    if(car == null) {
        return DEFAULT_INSTANCE_NAME;
    }
    Insurance insurance = car.getInsurance();
    if(insurance == null) {
        return DEFAULT_INSTANCE_NAME;
    }
    return insurance.getName();
}

上面代碼雖然解決了嵌套過深問題,然而,這種方案遠非理想,現(xiàn)在這個方法有了四個截然不同的退出點,使得代碼的維護異常艱難。而且這種流程是極易出錯的;如果你忘記檢查了那個可能為null的屬性會怎樣

null 帶來的種種問題

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

其他語言中null的替代品

  • Groovy : 通過引入安全導航操作符(Safe Navigation Operator,標記為?)可以安全訪問可能為null的變量.
    exp : def carInsuranceName = person?.car?.insurance?.name
    Groovy的安全導航操作符能夠避免在訪問這些可能為null引用的變量時拋出NullPointerException,在調(diào)用鏈中的變量遭遇null時將null引用沿著調(diào)用鏈傳遞下去,返回一個null。
  • Haskell中包含了一個Maybe類型,它本質(zhì)上是對optional值的封裝。Maybe類型的變量可以是指定類型的值,也可以什么都不是。但是它并沒有null引用的概念。Scala有類似的數(shù)據(jù)結構,名字叫Option[T],它既可以包含類型為T的變量,也可以不包含該變量; 要使用這種類型,你必須顯式地調(diào)用Option類型的available操作,檢查該變量是否有值,而這其實也是一種變相的“null檢查”

java8 Optional

汲取Haskell和Scala的靈感, Java 8中引入了一個新的類java.util.Optional<T>。這是一個封裝Optional值的類。變量存在時, Optional類只是對類簡單封裝。變量不存在時,缺失的值會被建模成一個“空”的Optional對象,由方法Optional.empty()返回。Optional.empty()方法是一個靜態(tài)工廠方法,它返回Optional類的特定單一實例。

null引用和Optional.empty()有什么本質(zhì)的區(qū)別嗎?

  1. 不會觸發(fā)NullPointerException。
  2. 使用Optional而不是null的一個非常重要而又實際的語義區(qū)別是:如聲明變量時使用的是Optional<Car>類型,而不是Car類型,這句聲明非常清楚地表明了這發(fā)生變量缺失是允許的;與此相反,使用Car這樣的類型,可能將變量賦值為null,這意味著你需要獨立面對這些,你只能依賴你對業(yè)務模型的理解,判斷一個null是否屬于該變量的有效范疇。

你的代碼中始終如一地使用Optional,能非常清晰地界定出變量值的缺失是結構上的問題,還是你算法上的缺陷,抑或是你數(shù)據(jù)中的問題。另外,我們還想特別強調(diào),引入Optional類的意圖并非要消除每一個null引用。與此相反,它的目標是幫助你更好地設計出普適的API,讓程序員看到方法簽名,就能了解它是否接受一個Optional的值。這種強制會讓你更積極地將變量從Optional中解包出來,直面缺失的變量值。

應用 Optional 的幾種模式

創(chuàng)建Optional對象

  1. 聲明一個空的Optional
Optional<Car> optCar = Optional.empty();
  1. 依據(jù)一個非空值創(chuàng)建Optional
Optional<Car> optCar = Optional.of(car);

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

  1. 可接受null的Optional
Optional<Car> optCar = Optional.ofNullable(car);
  1. 使用 map 從 Optional 對象中提取和轉換值
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
  1. 使用 flatMap 從 Optional 對象提取值
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
String name = optInsurance.flatMap(Insurance::getName);

那對上面的例子可以重新建模 exp:

public class Car {
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }

    public void setInsurance(Optional<Insurance> insurance) {
        this.insurance = insurance;
    }
}
////////////////
public class Person {

    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }

    public void setCar(Optional<Car> car) {
        this.car = car;
    }
}
//////////////////////
public class Insurance {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

獲取保險公司名稱,如何使用Optional獲取呢? exp:

public String getInstanceName1(Optional<Person> person) {
    if(person.isPresent()) {
        Optional<Car> car = person.get().getCar();
        if(car.isPresent()) {
            Optional<Insurance> insurance = car.get().getInsurance();
            if(insurance.isPresent()) {
                return insurance.get().getName();
            }
        }
    }
    return UNKNOWN_INSTANCE_NAME;
}

這種方式和null判斷本質(zhì)是一樣的,沒有解決問題。

**真正的解法 使用flatMap,map 來提取轉換值 exp: **

public String getInstanceName2(Optional<Person> person) {
    return person.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .orElse(UNKNOWN_INSTANCE_NAME);
}

我們決定采用orElse方法讀取這個變量的值,使用這種方式你還可以定義一個默認值,遭遇空的Optional變量時,默認值會作為該方法的調(diào)用返回值。Optional類提供了多種方法讀取Optional實例中的變量值。

  • get() 是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接返回封裝的變量值,否則就拋出一個NoSuchElementException異常。所以,除非你非常確定Optional變量一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對于嵌套式的null檢查,也并未體現(xiàn)出多大的改進。
  • orElse(T other) 它允許你在Optional對象不包含值時提供一個默認值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延遲調(diào)用版,Supplier方法只有在Optional對象不含值時才執(zhí)行調(diào)用。如果創(chuàng)建默認值是件耗時費力的工作,你應該考慮采用這種方式(借此提升程序的性能),或者你需要非常確定某個方法僅在Optional為空時才進行調(diào)用,也可以考慮該方式(這種情況有嚴格的限制條件)。
  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似,它們遭遇Optional對象為空時都會拋出一個異常,但是使用orElseThrow你可以定制希望拋出的異常類型
  • ifPresent(Consumer<? super T>) 讓你能在變量值存在時執(zhí)行一個作為參數(shù)傳入的方法,否則就不進行任何操作。

兩個 Optional 對象的組合

假設你有這樣一個方法,它接受一個Person和一個Car對象,并以此為條件對外
部提供的服務進行查詢,通過一些復雜的業(yè)務邏輯,試圖找到滿足該組合的最便宜的保險公司:

public Insurance findCheapestInsurance(Person person, Car car) {
    // 不同的保險公司提供的查詢服務
    // 對比所有數(shù)據(jù)
    return cheapestCompany;
}

設你想要該方法的一個null-安全的版本,它接受兩個Optional對象作為參數(shù),
返回值是一個Optional<Insurance>對象,如果傳入的任何一個參數(shù)值為空,它的返回值亦為空 exp:

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

該方法的具體實現(xiàn)和你之前曾經(jīng)實現(xiàn)的null檢查太相似了,有沒有更優(yōu)雅的方案呢?
我們可以像使用三元操作符那樣,無需任何條件判斷的結構,以一行語句實現(xiàn)該方法,代碼如下。exp:

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

這段代碼中,你對第一個Optional對象調(diào)用flatMap方法,如果它是個空值,傳遞給它的Lambda表達式不會執(zhí)行,這次調(diào)用會直接返回一個空的Optional對象。反之,如果person對象存在,這次調(diào)用就會將其作為函數(shù)Function的輸入,并按照與flatMap方法的約定返回一個Optional<Insurance>對象。這個函數(shù)的函數(shù)體會對第二個Optional對象執(zhí)行map操作,如果第二個對象不包含car,函數(shù)Function就返回一個空的Optional對象,整個nullSafeFindCheapestInsuranc方法的返回值也是一個空的Optional對象。最后,如果person和car對象都存在,作為參數(shù)傳遞給map方法的Lambda表達式能夠使用這兩個值安全地調(diào)用原始的findCheapestInsurance方法,完成期望的操作。

Optional類的方法

方法 描述
empty 返回一個空的 Optional 實例
filter 如果值存在并且滿足提供的謂詞,就返回包含該值的 Optional 對象;否則返回一個空的Optional 對象
flatMap 如果值存在,就對該值執(zhí)行提供的 mapping 函數(shù)調(diào)用,返回一個 Optional 類型的值,否則就返回一個空的 Optional 對象
get 如果該值存在,將該值用 Optional 封裝返回,否則拋出一個 NoSuchElementException 異常
ifPresent 如果值存在,就執(zhí)行使用該值的方法調(diào)用,否則什么也不做
isPresent 如果值存在就返回 true,否則返回 false
map 如果值存在,就對該值執(zhí)行提供的 mapping 函數(shù)調(diào)用
of 將指定值用 Optional 封裝之后返回,如果該值為 null,則拋出一個 NullPointerException異常
ofNullable 將指定值用 Optional 封裝之后返回,如果該值為 null,則返回一個空的 Optional 對象
orElse 如果有值則將其返回,否則返回一個默認值
orElseGet 如果有值則將其返回,否則返回一個由指定的 Supplier 接口生成的值
orElseThrow 如果有值則將其返回,否則拋出一個由指定的 Supplier 接口生成的異常

使用 Optional 的實戰(zhàn)示例

有效地使用Optional類意味著你需要對如何處理潛在缺失值進行全面的反思。這種反思不僅僅限于你曾經(jīng)寫過的代碼,更重要的可能是,你如何與原生Java API實現(xiàn)共存共贏。實際上,我們相信如果Optional類能夠在這些API創(chuàng)建之初就存在的話,很多API的設計編寫可能會大有不同。為了保持后向兼容性,我們很難對老的Java API進行改動,讓它們也使用Optional,但這并不表示我們什么也做不了。你可以在自己的代碼中添加一些工具方法,修復或者繞過這些問題,讓你的代碼能享受Optional帶來的威力。exp:

  1. 用 Optional 封裝可能為 null 的值,比如從map獲取值
Optional<Object> value = Optional.ofNullable(map.get("key"))

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

  1. 異常與 Optional 的對比
    由于某種原因,函數(shù)無法返回某個值,這時除了返回null, Java API比較常見的替代做法是拋出一個異常。exp: Integer.parseInt(String)如果String無法解析到對應的整型,該方法就拋出一個NumberFormatException;我們可以空的Optional對象,對遭遇無法轉換的String時返回的非法值進行建模。exp:
public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

我們可以將多個類似的方法封裝到一個工具類中,如OptionalUtility.stringToInt.

注: 與 Stream 對象一樣,Optional也提供了類似的基礎類型——OptionalInt、 OptionalLong以及OptionalDouble。在Stream的場景,尤其是如果Stream對象包含了大量元素,出于性能的考量,使用基礎類型是不錯的選擇,但對Optional對象而言,這個理由就不成立了,因為Optional對象最多只包含一個值。不推薦大家使用基礎類型的Optional,因為基礎類型的Optional不支持map、flatMap以及filter方法,而這些卻是Optional類最有用的方法,此外,與Stream一樣, Optional對象無法由基礎類型的Optional組合構成

把所有內(nèi)容整合起來

Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");

現(xiàn)在,我們假設你的程序需要從這些屬性中讀取一個值,該值是以秒為單位計量的一段時間。由于一段時間必須是正數(shù),你想要該方法符合下面的簽名:

public int readDuration(Properties props, String name)

如果以命令式編程的方式從屬性中讀取duration值,exp:

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

可以看出最終的實現(xiàn)既復雜又不具備可讀性,呈現(xiàn)為多個由if語句及try/catch
塊兒構成的嵌套條件。

以Optional來實現(xiàn)呢exp:

public int readDuration(Properties props, String name) {
    return Optional.ofNullable(props.getProperty(name))
        .flatMap(OptionalUtility::stringToInt)
        .orElse(0);
}

Optional 問題

在域模型中使用Optional,由于沒有實現(xiàn)Serializable 接口,不能進行序列化

小結

  • null引用在歷史上被引入到程序設計語言中,目的是為了表示變量值的缺失。
  • Java 8中引入了一個新的類java.util.Optional<T>,對存在或缺失的變量值進行建模。
  • 你可以使用靜態(tài)工廠方法Optional.empty、Optional.of以及Optional.ofNullable創(chuàng)建Optional對象。
  • Optional類支持多種方法,比如map、flatMap、filter,它們在概念上與Stream類中對應的方法十分相似。
  • 使用Optional會迫使你更積極地解引用Optional對象,以應對變量值缺失的問題,最終,你能更有效地防止代碼中出現(xiàn)不期而至的空指針異常。
  • 使用Optional能幫助你設計更好的API,用戶只需要閱讀方法簽名,就能了解該方法是否接受一個Optional類型的值。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 這篇文章寫得不錯,所以轉載了下,修改了小部分,原文地址見末尾 身為一名Java程序員,大家可能都有這樣的經(jīng)歷:調(diào)用...
    瘋狂的冰塊閱讀 302評論 0 2
  • 轉載自 http://www.importnew.com/6675.html 身為一名Java程序員,大家可能都有...
    tenlee閱讀 538評論 0 0
  • Java 8引入了一個新的Optional類。Optional類的Javadoc描述如下: 這是一個可以為null...
    吧啦啦小湯圓閱讀 1,550評論 0 1
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,578評論 19 139
  • 懷斯曼
    風一米陽光閱讀 488評論 0 47

友情鏈接更多精彩內(nèi)容