Optional
本章內(nèi)容
- 如何為缺失的值建模
- Optional 類
- 應(yīng)用Optional的幾種模式
- 使用Optional的實戰(zhàn)實例
- 小結(jié)
如何為缺失的值建模
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方法的結(jié)果會怎樣呢痒谴?在實踐中,一種比較常見的做法是返回一個null引用采章,表示該值的缺失惧笛,即用戶沒有車。而接下來甘磨,對getInsurance的調(diào)用會返回null引用的insurance橡羞,這會導(dǎo)致運行時出現(xiàn)
一個NullPointerException,終止程序的運行济舆。但這還不是全部卿泽。如果返回的person值為null會怎樣?如果getInsurance的返回值也是null滋觉,結(jié)果又會怎樣签夭?
采用防御式檢查減少 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ì)疑”椎侠,原因是它不斷重復(fù)著一種模式:每次你不確定一
個變量是否為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的哲學(xué)蹲盘。
Java一直試圖避免讓程序員意識到指針的存在,唯一的例外是: null指針膳音。 - 它在Java的類型系統(tǒng)上開了個口子召衔。
null并不屬于任何類型,這意味著它可以被賦值給任意引用類型的變量祭陷。這會導(dǎo)致問題苍凛,原因是當這個變量被傳遞到系統(tǒng)中的另一個部分后,你將無法獲知這個null變量最初的賦值到底是什么類型兵志。
其他語言中null的替代品
- Groovy : 通過引入安全導(dǎo)航操作符(Safe Navigation Operator醇蝴,標記為?)可以安全訪問可能為null的變量.
exp :def carInsuranceName = person?.car?.insurance?.name
Groovy的安全導(dǎo)航操作符能夠避免在訪問這些可能為null引用的變量時拋出NullPointerException,在調(diào)用鏈中的變量遭遇null時將null引用沿著調(diào)用鏈傳遞下去想罕,返回一個null悠栓。 - Haskell中包含了一個Maybe類型,它本質(zhì)上是對optional值的封裝按价。Maybe類型的變量可以是指定類型的值惭适,也可以什么都不是。但是它并沒有null引用的概念楼镐。Scala有類似的數(shù)據(jù)結(jié)構(gòu)癞志,名字叫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è)務(wù)模型的理解,判斷一個null是否屬于該變量的有效范疇豪椿。
你的代碼中始終如一地使用Optional奔坟,能非常清晰地界定出變量值的缺失是結(jié)構(gòu)上的問題,還是你算法上的缺陷搭盾,抑或是你數(shù)據(jù)中的問題蛀蜜。另外,我們還想特別強調(diào)增蹭,引入Optional類的意圖并非要消除每一個null引用滴某。與此相反,它的目標是幫助你更好地設(shè)計出普適的API滋迈,讓程序員看到方法簽名霎奢,就能了解它是否接受一個Optional的值。這種強制會讓你更積極地將變量從Optional中解包出來饼灿,直面缺失的變量值幕侠。
應(yīng)用 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 對象中提取和轉(zhuǎn)換值
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 來提取轉(zhuǎn)換值 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)建默認值是件耗時費力的工作,你應(yīng)該考慮采用這種方式(借此提升程序的性能)损同,或者你需要非常確定某個方法僅在Optional為空時才進行調(diào)用翩腐,也可以考慮該方式(這種情況有嚴格的限制條件)。
- orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似膏燃,它們遭遇Optional對象為空時都會拋出一個異常茂卦,但是使用orElseThrow你可以定制希望拋出的異常類型
- ifPresent(Consumer<? super T>) 讓你能在變量值存在時執(zhí)行一個作為參數(shù)傳入的方法,否則就不進行任何操作组哩。
兩個 Optional 對象的組合
假設(shè)你有這樣一個方法等龙,它接受一個Person和一個Car對象,并以此為條件對外
部提供的服務(wù)進行查詢伶贰,通過一些復(fù)雜的業(yè)務(wù)邏輯蛛砰,試圖找到滿足該組合的最便宜的保險公司:
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保險公司提供的查詢服務(wù)
// 對比所有數(shù)據(jù)
return cheapestCompany;
}
設(shè)你想要該方法的一個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)雅的方案呢琅翻?
我們可以像使用三元操作符那樣位仁,無需任何條件判斷的結(jié)構(gò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的設(shè)計編寫可能會大有不同盗誊。為了保持后向兼容性,我們很難對老的Java API進行改動,讓它們也使用Optional哈踱,但這并不表示我們什么也做不了荒适。你可以在自己的代碼中添加一些工具方法,修復(fù)或者繞過這些問題开镣,讓你的代碼能享受Optional帶來的威力刀诬。exp:
- 用 Optional 封裝可能為 null 的值,比如從map獲取值
Optional<Object> value = Optional.ofNullable(map.get("key"))
每次你希望安全地對潛在為null的對象進行轉(zhuǎn)換,將其替換為Optional對象時邪财,都可以考慮使用這種方法陕壹。
- 異常與 Optional 的對比
由于某種原因,函數(shù)無法返回某個值树埠,這時除了返回null糠馆, Java API比較常見的替代做法是拋出一個異常。exp:Integer.parseInt(String)
如果String無法解析到對應(yīng)的整型怎憋,該方法就拋出一個NumberFormatException;我們可以空的Optional對象又碌,對遭遇無法轉(zhuǎn)換的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也提供了類似的基礎(chǔ)類型——OptionalInt赠橙、 OptionalLong以及OptionalDouble。在Stream的場景愤炸,尤其是如果Stream對象包含了大量元素期揪,出于性能的考量,使用基礎(chǔ)類型是不錯的選擇规个,但對Optional對象而言凤薛,這個理由就不成立了,因為Optional對象最多只包含一個值诞仓。不推薦大家使用基礎(chǔ)類型的Optional缤苫,因為基礎(chǔ)類型的Optional不支持map、flatMap以及filter方法墅拭,而這些卻是Optional類最有用的方法活玲,此外,與Stream一樣谍婉, Optional對象無法由基礎(chǔ)類型的Optional組合構(gòu)成
把所有內(nèi)容整合起來
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
現(xiàn)在舒憾,我們假設(shè)你的程序需要從這些屬性中讀取一個值,該值是以秒為單位計量的一段時間穗熬。由于一段時間必須是正數(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)既復(fù)雜又不具備可讀性唤蔗,呈現(xiàn)為多個由if語句及try/catch
塊兒構(gòu)成的嵌套條件探遵。
以O(shè)ptional來實現(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 接口,不能進行序列化
小結(jié)
- null引用在歷史上被引入到程序設(shè)計語言中箱季,目的是為了表示變量值的缺失涯穷。
- Java 8中引入了一個新的類java.util.Optional<T>,對存在或缺失的變量值進行建模规哪。
- 你可以使用靜態(tài)工廠方法Optional.empty求豫、Optional.of以及Optional.ofNullable創(chuàng)建Optional對象塌衰。
- Optional類支持多種方法诉稍,比如map、flatMap最疆、filter杯巨,它們在概念上與Stream類中對應(yīng)的方法十分相似。
- 使用Optional會迫使你更積極地解引用Optional對象努酸,以應(yīng)對變量值缺失的問題服爷,最終,你能更有效地防止代碼中出現(xiàn)不期而至的空指針異常获诈。
- 使用Optional能幫助你設(shè)計更好的API仍源,用戶只需要閱讀方法簽名,就能了解該方法是否接受一個Optional類型的值舔涎。