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ū)別嗎?
- 不會觸發(fā)NullPointerException。
- 使用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對象
- 聲明一個空的Optional
Optional<Car> optCar = Optional.empty();
- 依據(jù)一個非空值創(chuàng)建Optional
Optional<Car> optCar = Optional.of(car);
如果car是一個null,這段代碼會立即拋出一個NullPointerException,而不是等到你試圖訪問car的屬性值時才返回一個錯誤。
- 可接受null的Optional
Optional<Car> optCar = Optional.ofNullable(car);
- 使用 map 從 Optional 對象中提取和轉換值
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
- 使用 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:
- 用 Optional 封裝可能為 null 的值,比如從map獲取值
Optional<Object> value = Optional.ofNullable(map.get("key"))
每次你希望安全地對潛在為null的對象進行轉換,將其替換為Optional對象時,都可以考慮使用這種方法。
- 異常與 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類型的值。