摘要:是第一批在堆上分配記錄的類型語言之一。實(shí)際上,的這段話低估了過去五十年來數(shù)百萬程序員為修復(fù)空引用所耗費(fèi)的代價(jià)。很明顯,這種方式不具備擴(kuò)展性,同時(shí)還犧牲了代碼的可讀性。是目前程序開發(fā)中最典型的異常。完成這一任務(wù)有多種方法。
用Optional取代null
如果你作為Java程序員曾經(jīng)遭遇過NullPointerException,請(qǐng)舉起手。如果這是你最常遭遇的異常,請(qǐng)繼續(xù)舉手。非??上?,這個(gè)時(shí)刻,我們無法看到對(duì)方,但是我相信很多人的手這個(gè)時(shí)刻是舉著的。我們還猜想你可能也有這樣的想法:“毫無疑問,我承認(rèn),對(duì)任何一位Java程序員來說,無論是初出茅廬的新人,還是久經(jīng)江湖的專家,NullPointerException都是他心中的痛,可是我們又無能為力,因?yàn)檫@就是我們?yōu)榱耸褂梅奖闵踔敛豢杀苊獾南駈ull引用這樣的構(gòu)造所付出的代價(jià)?!边@就是程序設(shè)計(jì)世界里大家都持有的觀點(diǎn),然而,這可能并非事實(shí)的全部真相,只是我們根深蒂固的一種偏見。
1965年,英國一位名為Tony Hoare的計(jì)算機(jī)科學(xué)家在設(shè)計(jì)ALGOL W語言時(shí)提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的類型語言之一。Hoare選擇null引用這種方式,“只是因?yàn)檫@種方法實(shí)現(xiàn)起來非常容易”。雖然他的設(shè)計(jì)初衷就是要“通過編譯器的自動(dòng)檢測(cè)機(jī)制,確保所有使用引用的地方都是絕對(duì)安全的”,他還是決定為null引用開個(gè)綠燈,因?yàn)樗J(rèn)為這是為“不存在的值”建模最容易的方式。很多年后,他開始為自己曾經(jīng)做過這樣的決定而后悔不迭,把它稱為“我價(jià)值百萬的重大失誤”。我們已經(jīng)看到它帶來的后果——程序員對(duì)對(duì)象的字段進(jìn)行檢查,判斷它的值是否為期望的格式,最終卻發(fā)現(xiàn)我們查看的并不是一個(gè)對(duì)象,而是一個(gè)空指針,它會(huì)立即拋出一個(gè)讓人厭煩的NullPointerException異常。
實(shí)際上,Hoare的這段話低估了過去五十年來數(shù)百萬程序員為修復(fù)空引用所耗費(fèi)的代價(jià)。近十年出現(xiàn)的大多數(shù)現(xiàn)代程序設(shè)計(jì)語言,包括Java,都采用了同樣的設(shè)計(jì)方式,其原因是為了與更老的語言保持兼容,或者就像Hoare曾經(jīng)陳述的那樣,“僅僅是因?yàn)檫@樣實(shí)現(xiàn)起來更加容易”。讓我們從一個(gè)簡(jiǎn)單的例子入手,看看使用null都有什么樣的問題。
如何為缺失的值建模假設(shè)你需要處理下面這樣的嵌套對(duì)象,這是一個(gè)擁有汽車及汽車保險(xiǎn)的客戶。
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(); }
這段代碼看起來相當(dāng)正常,但是現(xiàn)實(shí)生活中很多人沒有車。所以調(diào)用getCar方法的結(jié)果會(huì)怎樣呢?在實(shí)踐中,一種比較常見的做法是返回一個(gè)null引用,表示該值的缺失,即用戶沒有車。而接下來,對(duì)getInsurance的調(diào)用會(huì)返回null引用的insurance,這會(huì)導(dǎo)致運(yùn)行時(shí)出現(xiàn)一個(gè)NullPointerException,終止程序的運(yùn)行。但這還不是全部。如果返回的person值為null會(huì)怎樣?如果getInsurance的返回值也是null,結(jié)果又會(huì)怎樣?
采用防御式檢查減少NullPointerException怎樣做才能避免這種不期而至的NullPointerException呢?通常,你可以在需要的地方添加null的檢查(過于激進(jìn)的防御式檢查甚至?xí)诓惶枰牡胤教砑訖z測(cè)代碼),并且添加的方式往往各有不同。下面這個(gè)例子是我們?cè)噲D在方法中避免NullPointerException的第一次嘗試。
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"; }
這個(gè)方法每次引用一個(gè)變量都會(huì)做一次null檢查,如果引用鏈上的任何一個(gè)遍歷的解變量值為null,它就返回一個(gè)值為“Unknown”的字符串。唯一的例外是保險(xiǎn)公司的名字,你不需要對(duì)它進(jìn)行檢查,原因很簡(jiǎn)單,因?yàn)槿魏我患夜颈囟ㄓ袀€(gè)名字。注意到了嗎,由于你掌握業(yè)務(wù)領(lǐng)域的知識(shí),避免了最后這個(gè)檢查,但這并不會(huì)直接反映在你建模數(shù)據(jù)的Java類之中。
我們將上面的代碼標(biāo)記為“深層質(zhì)疑”,原因是它不斷重復(fù)著一種模式:每次你不確定一個(gè)變量是否為null時(shí),都需要添加一個(gè)進(jìn)一步嵌套的if塊,也增加了代碼縮進(jìn)的層數(shù)。很明顯,這種方式不具備擴(kuò)展性,同時(shí)還犧牲了代碼的可讀性。面對(duì)這種窘境,你也許愿意嘗試另一種方案。下面的代碼清單中,我們?cè)噲D通過一種不同的方式避免這種問題。
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(); }
第二種嘗試中,你試圖避免深層遞歸的if語句塊,采用了一種不同的策略:每次你遭遇null變量,都返回一個(gè)字符串常量“Unknown”。然而,這種方案遠(yuǎn)非理想,現(xiàn)在這個(gè)方法有了四個(gè)截然不同的退出點(diǎn),使得代碼的維護(hù)異常艱難。更糟的是,發(fā)生null時(shí)返回的默認(rèn)值,即字符串“Unknown”在三個(gè)不同的地方重復(fù)出現(xiàn)——出現(xiàn)拼寫錯(cuò)誤的概率不??!當(dāng)然,你可能會(huì)說,我們可以用把它們抽取到一個(gè)常量中的方式避免這種問題。
進(jìn)一步而言,這種流程是極易出錯(cuò)的;如果你忘記檢查了那個(gè)可能為null的屬性會(huì)怎樣?通過這一章的學(xué)習(xí),你會(huì)了解使用null來表示變量值的缺失是大錯(cuò)特錯(cuò)的。你需要更優(yōu)雅的方式來對(duì)缺失的變量值建模。
null 帶來的種種問題讓我們一起回顧一下到目前為止進(jìn)行的討論,在Java程序開發(fā)中使用null會(huì)帶來理論和實(shí)際操作上的種種問題。
它是錯(cuò)誤之源。NullPointerException是目前Java程序開發(fā)中最典型的異常。
它會(huì)使你的代碼膨脹。它讓你的代碼充斥著深度嵌套的null檢查,代碼的可讀性糟糕透頂。
它自身是毫無意義的。null自身沒有任何的語義,尤其是,它代表的是在靜態(tài)類型語言中以一種錯(cuò)誤的方式對(duì)缺失變量值的建模。
它破壞了Java的哲學(xué)。Java一直試圖避免讓程序員意識(shí)到指針的存在,唯一的例外是:null指針。
它在Java的類型系統(tǒng)上開了個(gè)口子。null并不屬于任何類型,這意味著它可以被賦值給任意引用類型的變量。這會(huì)導(dǎo)致問題,原因是當(dāng)這個(gè)變量被傳遞到系統(tǒng)中的另一個(gè)部分后,你將無法獲知這個(gè)null變量最初的賦值到底是什么類型。
Optional 類入門為了更好的解決和避免NPE異常,Java 8中引入了一個(gè)新的類java.util.Optional
變量存在時(shí),Optional類只是對(duì)類簡(jiǎn)單封裝。變量不存在時(shí),缺失的值會(huì)被建模成一個(gè)“空”的Optional對(duì)象,由方法Optional.empty()返回。Optional.empty()方法是一個(gè)靜態(tài)工廠方法,它返回Optional類的特定單一實(shí)例。你可能還有疑惑,null引用和Optional.empty()有什么本質(zhì)的區(qū)別嗎?從語義上,你可以把它們當(dāng)作一回事兒,但是實(shí)際中它們之間的差別非常大: 如果你嘗試解引用一個(gè)null , 一定會(huì)觸發(fā)NullPointerException , 不過使用Optional.empty()就完全沒事兒,它是Optional類的一個(gè)有效對(duì)象,多種場(chǎng)景都能調(diào)用,非常有用。關(guān)于這一點(diǎn),接下來的部分會(huì)詳細(xì)介紹。
使用Optional而不是null的一個(gè)非常重要而又實(shí)際的語義區(qū)別是,第一個(gè)例子中,我們?cè)诼暶髯兞繒r(shí)使用的是Optional
牢記上面這些原則,你現(xiàn)在可以使用Optional類對(duì)最初的代碼進(jìn)行重構(gòu),結(jié)果如下。
public class Person { private Optionalcar; public Optional getCar() { return car; } } public class Insurance { private String name; public String getName() { return name; } } public class Car { private Optional insurance; public Optional getInsurance() { return insurance; } }
發(fā)現(xiàn)Optional是如何豐富你模型的語義了吧。代碼中person引用的是Optional
與此同時(shí),我們看到insurance公司的名稱被聲明成String類型,而不是Optional
在你的代碼中始終如一地使用Optional,能非常清晰地界定出變量值的缺失是結(jié)構(gòu)上的問題,還是你算法上的缺陷,抑或是你數(shù)據(jù)中的問題。另外,我們還想特別強(qiáng)調(diào),引入Optional類的意圖并非要消除每一個(gè)null引用。與此相反,它的目標(biāo)是幫助你更好地設(shè)計(jì)出普適的API,讓程序員看到方法簽名,就能了解它是否接受一個(gè)Optional的值。這種強(qiáng)制會(huì)讓你更積極地將變量從Optional中解包出來,直面缺失的變量值。
應(yīng)用Optional 的幾種模式到目前為止,一切都很順利;你已經(jīng)知道了如何使用Optional類型來聲明你的域模型,也了解了這種方式與直接使用null引用表示變量值的缺失的優(yōu)劣。但是,我們?cè)撊绾问褂媚??用這種方式能做什么,或者怎樣使用Optional封裝的值呢?
創(chuàng)建Optional 對(duì)象使用Optional之前,你首先需要學(xué)習(xí)的是如何創(chuàng)建Optional對(duì)象。完成這一任務(wù)有多種方法。
聲明一個(gè)空的Optional
正如前文已經(jīng)提到,你可以通過靜態(tài)工廠方法Optional.empty,創(chuàng)建一個(gè)空的Optional對(duì)象:
OptionaloptCar = Optional.empty();
依據(jù)一個(gè)非空值創(chuàng)建Optional
你還可以使用靜態(tài)工廠方法Optional.of,依據(jù)一個(gè)非空值創(chuàng)建一個(gè)Optional對(duì)象:
OptionaloptCar = Optional.of(car);
如果car是一個(gè)null,這段代碼會(huì)立即拋出一個(gè)NullPointerException,而不是等到你試圖訪問car的屬性值時(shí)才返回一個(gè)錯(cuò)誤。
可接受null的Optional
最后,使用靜態(tài)工廠方法Optional.ofNullable,你可以創(chuàng)建一個(gè)允許null值的Optional對(duì)象:
OptionaloptCar = Optional.ofNullable(car);
如果car是null,那么得到的Optional對(duì)象就是個(gè)空對(duì)象。
你可能已經(jīng)猜到,我們還需要繼續(xù)研究“如何獲取Optional變量中的值”。尤其是,Optional提供了一個(gè)get方法,它能非常精準(zhǔn)地完成這項(xiàng)工作,我們?cè)诤竺鏁?huì)詳細(xì)介紹這部分內(nèi)容。不過get方法在遭遇到空的Optional對(duì)象時(shí)也會(huì)拋出異常,所以不按照約定的方式使用它,又會(huì)讓我們?cè)俣认萑胗蒼ull引起的代碼維護(hù)的夢(mèng)魘。因此,我們首先從無需顯式檢查的Optional值的使用入手,這些方法與Stream中的某些操作極其相似。
使用map 從Optional 對(duì)象中提取和轉(zhuǎn)換值從對(duì)象中提取信息是一種比較常見的模式。比如,你可能想要從insurance公司對(duì)象中提取公司的名稱。提取名稱之前,你需要檢查insurance對(duì)象是否為null,代碼如下所示:
String name = null; if(insurance != null){ name = insurance.getName(); }
為了支持這種模式,Optional提供了一個(gè)map方法。它的工作方式如下:
OptionaloptionalInsurance = Optional.ofNullable(insurance); Optional name = optionalInsurance.map(Insurance::getName);
從概念上,這與我們?cè)诘?章和第5章中看到的流的map方法相差無幾。map操作會(huì)將提供的函數(shù)應(yīng)用于流的每個(gè)元素。你可以把Optional對(duì)象看成一種特殊的集合數(shù)據(jù),它至多包含一個(gè)元素。如果Optional包含一個(gè)值,那函數(shù)就將該值作為參數(shù)傳遞給map,對(duì)該值進(jìn)行轉(zhuǎn)換。如果Optional為空,就什么也不做。
這看起來挺有用,但是你怎樣才能應(yīng)用起來,重構(gòu)之前的代碼呢?前文的代碼里用安全的方式鏈接了多個(gè)方法。
public String getCarInsuranceName(Person person) { return person.getCar().getInsurance().getName(); }
為了達(dá)到這個(gè)目的,我們需要求助Optional提供的另一個(gè)方法flatMap。
使用flatMap 鏈接Optional 對(duì)象由于我們剛剛學(xué)習(xí)了如何使用map,你的第一反應(yīng)可能是我們可以利用map重寫之前的代碼,如下所示:
OptionaloptPerson = Optional.of(person); Optional name = optPerson.map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName);
不幸的是,這段代碼無法通過編譯。為什么呢?optPerson是Optional
所以,我們?cè)撊绾谓鉀Q這個(gè)問題呢?讓我們?cè)倩仡櫼幌履銊倓傇诹魃鲜褂眠^的模式:flatMap方法。使用流時(shí),flatMap方法接受一個(gè)函數(shù)作為參數(shù),這個(gè)函數(shù)的返回值是另一個(gè)流。這個(gè)方法會(huì)應(yīng)用到流中的每一個(gè)元素,最終形成一個(gè)新的流的流。但是flagMap會(huì)用流的內(nèi)容替換每個(gè)新生成的流。換句話說,由方法生成的各個(gè)流會(huì)被合并或者扁平化為一個(gè)單一的流。這里你希望的結(jié)果其實(shí)也是類似的,但是你想要的是將兩層的optional合并為一個(gè)。
這個(gè)例子中,傳遞給流的flatMap方法會(huì)將每個(gè)正方形轉(zhuǎn)換為另一個(gè)流中的兩個(gè)三角形。那么,map操作的結(jié)果就包含有三個(gè)新的流,每一個(gè)流包含兩個(gè)三角形,但flatMap方法會(huì)將這種兩層的流合并為一個(gè)包含六個(gè)三角形的單一流。類似地,傳遞給optional的flatMap方法的函數(shù)會(huì)將原始包含正方形的optional對(duì)象轉(zhuǎn)換為包含三角形的optional對(duì)象。如果將該方法傳遞給map方法,結(jié)果會(huì)是一個(gè)Optional對(duì)象,而這個(gè)Optional對(duì)象中包含了三角形;但flatMap方法會(huì)將這種兩層的Optional對(duì)象轉(zhuǎn)換為包含三角形的單一Optional對(duì)象。
使用Optional獲取car的保險(xiǎn)公司名稱
相信現(xiàn)在你已經(jīng)對(duì)Optional的map和flatMap方法有了一定的了解,讓我們看看如何應(yīng)用。
public String getCarInsuranceName(Optionalperson) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) // 如果Optional的j結(jié)果為空值,設(shè)置默認(rèn)值 .orElse("Unknown"); }
我們可以看到,處理潛在可能缺失的值時(shí),使用Optional具有明顯的優(yōu)勢(shì)。這一次,你可以用非常容易卻又普適的方法實(shí)現(xiàn)之前你期望的效果——不再需要使用那么多的條件分支,也不會(huì)增加代碼的復(fù)雜性。
使用Optional解引用串接的Person/Car/Insurance對(duì)象
由Optional
這里,我們從以O(shè)ptional封裝的Person入手,對(duì)其調(diào)用flatMap(Person::getCar)。如前所述,這種調(diào)用邏輯上可以劃分為兩步。第一步,某個(gè)Function作為參數(shù),被傳遞給由Optional封裝的Person對(duì)象,對(duì)其進(jìn)行轉(zhuǎn)換。這個(gè)場(chǎng)景中,F(xiàn)unction的具體表現(xiàn)是一個(gè)方法引用,即對(duì)Person對(duì)象的getCar方法進(jìn)行調(diào)用。由于該方法返回一個(gè)Optional
第二步與第一步大同小異,它會(huì)將Optional
截至目前為止,返回的Optional可能是兩種情況:如果調(diào)用鏈上的任何一個(gè)方法返回一個(gè)空的Optional,那么結(jié)果就為空,否則返回的值就是你期望的保險(xiǎn)公司的名稱。那么,你如何讀出這個(gè)值呢?畢竟你最后得到的這個(gè)對(duì)象還是個(gè)Optional
我們決定采用orElse方法讀取這個(gè)變量的值,使用這種方式你還可以定義一個(gè)默認(rèn)值,遭遇空的Optional變量時(shí),默認(rèn)值會(huì)作為該方法的調(diào)用返回值。Optional類提供了多種方法讀取Optional實(shí)例中的變量值。
get()是這些方法中最簡(jiǎn)單但又最不安全的方法。如果變量存在,它直接返回封裝的變量值,否則就拋出一個(gè)NoSuchElementException異常。所以,除非你非常確定Optional變量一定包含值,否則使用這個(gè)方法是個(gè)相當(dāng)糟糕的主意。此外,這種方式即便相對(duì)于嵌套式的null檢查,也并未體現(xiàn)出多大的改進(jìn)。
orElse(T other)是我們?cè)诖a使用的方法,正如之前提到的,它允許你在Optional對(duì)象不包含值時(shí)提供一個(gè)默認(rèn)值。
orElseGet(Supplier extends T> other)是orElse方法的延遲調(diào)用版,Supplier方法只有在Optional對(duì)象不含值時(shí)才執(zhí)行調(diào)用。如果創(chuàng)建默認(rèn)值是件耗時(shí)費(fèi)力的工作,你應(yīng)該考慮采用這種方式(借此提升程序的性能),或者你需要非常確定某個(gè)方法僅在Optional為空時(shí)才進(jìn)行調(diào)用,也可以考慮該方式(這種情況有嚴(yán)格的限制條件)。
orElseThrow(Supplier extends X> exceptionSupplier)和get方法非常類似,它們?cè)庥鯫ptional對(duì)象為空時(shí)都會(huì)拋出一個(gè)異常,但是使用orElseThrow你可以定制希望拋出的異常類型。
ifPresent(Consumer super T>)讓你能在變量值存在時(shí)執(zhí)行一個(gè)作為參數(shù)傳入的方法,否則就不進(jìn)行任何操作。
Optional類和Stream接口的相似之處,遠(yuǎn)不止map和flatMap這兩個(gè)方法。還有第三個(gè)方法filter,它的行為在兩種類型之間也極其相似。
兩個(gè)Optional 對(duì)象的組合現(xiàn)在,我們假設(shè)你有這樣一個(gè)方法,它接受一個(gè)Person和一個(gè)Car對(duì)象,并以此為條件對(duì)外部提供的服務(wù)進(jìn)行查詢,通過一些復(fù)雜的業(yè)務(wù)邏輯,試圖找到滿足該組合的最便宜的保險(xiǎn)公司:
public Insurance findCheapestInsurance(Person person, Car car) { // 不同的保險(xiǎn)公司提供的查詢服務(wù) // 對(duì)比所有數(shù)據(jù) return cheapestCompany; }
我們還假設(shè)你想要該方法的一個(gè)null-安全的版本,它接受兩個(gè)Optional對(duì)象作為參數(shù),返回值是一個(gè)Optional
public OptionalnullSafeFindCheapestInsurance(Optional person, Optional car) { if (person.isPresent() && car.isPresent()) { return Optional.of(findCheapestInsurance(person.get(), car.get())); } else { return Optional.empty(); } }
這個(gè)方法具有明顯的優(yōu)勢(shì),我們從它的簽名就能非常清楚地知道無論是person還是car,它的值都有可能為空,出現(xiàn)這種情況時(shí),方法的返回值也不會(huì)包含任何值。不幸的是,該方法的具體實(shí)現(xiàn)和你之前曾經(jīng)實(shí)現(xiàn)的null檢查太相似了:方法接受一個(gè)Person和一個(gè)Car對(duì)象作為參數(shù),而二者都有可能為null。利用Optional類提供的特性,有沒有更好或更地道的方式來實(shí)現(xiàn)這個(gè)方法呢?
Optional類和Stream接口的相似之處遠(yuǎn)不止map和flatMap這兩個(gè)方法。還有第三個(gè)方法filter,它的行為在兩種類型之間也極其相似,我們?cè)诮酉聛淼囊还?jié)會(huì)進(jìn)行介紹。
使用filter 剔除特定的值你經(jīng)常需要調(diào)用某個(gè)對(duì)象的方法,查看它的某些屬性。比如,你可能需要檢查保險(xiǎn)公司的名稱是否為“Cambridge-Insurance”。為了以一種安全的方式進(jìn)行這些操作,你首先需要確定引用指向的Insurance對(duì)象是否為null,之后再調(diào)用它的getName方法,如下所示:
Insurance insurance = ...; if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ System.out.println("ok"); }
使用Optional對(duì)象的filter方法,這段代碼可以重構(gòu)如下:
OptionaloptInsurance = ...; optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) .ifPresent(x -> System.out.println("ok"));
filter方法接受一個(gè)謂詞作為參數(shù)。如果Optional對(duì)象的值存在,并且它符合謂詞的條件,filter方法就返回其值;否則它就返回一個(gè)空的Optional對(duì)象。如果你還記得我們可以將Optional看成最多包含一個(gè)元素的Stream對(duì)象,這個(gè)方法的行為就非常清晰了。如果Optional對(duì)象為空,它不做任何操作,反之,它就對(duì)Optional對(duì)象中包含的值施加謂詞操作。如果該操作的結(jié)果為true,它不做任何改變,直接返回該Optional對(duì)象,否則就將該值過濾掉,將Optional的值置空。
下一節(jié)中,我們會(huì)探討Optional類剩下的一些特性,并提供更實(shí)際的例子,展示多種你能夠應(yīng)用于代碼中更好地管理缺失值的技巧。
使用Optional 的實(shí)戰(zhàn)示例相信你已經(jīng)了解,有效地使用Optional類意味著你需要對(duì)如何處理潛在缺失值進(jìn)行全面的反思。這種反思不僅僅限于你曾經(jīng)寫過的代碼,更重要的可能是,你如何與原生Java API實(shí)現(xiàn)共存共贏。
實(shí)際上,我們相信如果Optional類能夠在這些API創(chuàng)建之初就存在的話,很多API的設(shè)計(jì)編寫可能會(huì)大有不同。為了保持后向兼容性,我們很難對(duì)老的Java API進(jìn)行改動(dòng),讓它們也使用Optional,但這并不表示我們什么也做不了。你可以在自己的代碼中添加一些工具方法,修復(fù)或者繞過這些問題,讓你的代碼能享受Optional帶來的威力。我們會(huì)通過幾個(gè)實(shí)際的例子講解如何達(dá)到這樣的目的。
用Optional 封裝可能為null 的值現(xiàn)存Java API幾乎都是通過返回一個(gè)null的方式來表示需要值的缺失,或者由于某些原因計(jì)算無法得到該值。比如,如果Map中不含指定的鍵對(duì)應(yīng)的值,它的get方法會(huì)返回一個(gè)null。但是,正如我們之前介紹的,大多數(shù)情況下,你可能希望這些方法能返回一個(gè)Optional對(duì)象。你無法修改這些方法的簽名,但是你很容易用Optional對(duì)這些方法的返回值進(jìn)行封裝。我們接著用Map做例子,假設(shè)你有一個(gè)Map
Object value = map.get("key");
使用Optional封裝map的返回值,你可以對(duì)這段代碼進(jìn)行優(yōu)化。要達(dá)到這個(gè)目的有兩種方式:你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會(huì)增加代碼的復(fù)雜度;或者你可以采用我們前文介紹的Optional.ofNullable方法:
Optional
每次你希望安全地對(duì)潛在為null的對(duì)象進(jìn)行轉(zhuǎn)換,將其替換為Optional對(duì)象時(shí),都可以考慮使用這種方法。
異常與Optional 的對(duì)比由于某種原因,函數(shù)無法返回某個(gè)值,這時(shí)除了返回null,Java API比較常見的替代做法是拋出一個(gè)異常。這種情況比較典型的例子是使用靜態(tài)方法Integer.parseInt(String),將String轉(zhuǎn)換為int。在這個(gè)例子中,如果String無法解析到對(duì)應(yīng)的整型,該方法就拋出一個(gè)NumberFormatException。最后的效果是,發(fā)生String無法轉(zhuǎn)換為int時(shí),代碼發(fā)出一個(gè)遭遇非法參數(shù)的信號(hào),唯一的不同是,這次你需要使用try/catch 語句,而不是使用if條件判斷來控制一個(gè)變量的值是否非空。
你也可以用空的Optional對(duì)象,對(duì)遭遇無法轉(zhuǎn)換的String時(shí)返回的非法值進(jìn)行建模,這時(shí)你期望parseInt的返回值是一個(gè)optional。我們無法修改最初的Java方法,但是這無礙我們進(jìn)行需要的改進(jìn),你可以實(shí)現(xiàn)一個(gè)工具方法,將這部分邏輯封裝于其中,最終返回一個(gè)我們希望的Optional對(duì)象,代碼如下所示。
public static OptionalstringToInt(String s) { try { return Optional.of(Integer.parseInt(s)); } catch (NumberFormatException e) { return Optional.empty(); } }
我們的建議是,你可以將多個(gè)類似的方法封裝到一個(gè)工具類中,讓我們稱之為OptionalUtility。通過這種OptionalUtility.stringToInt方法,將String轉(zhuǎn)換為一個(gè)Optional
Github: chap10
Gitee: chap10
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/72006.html
摘要:但返回的是一個(gè)類型的對(duì)象,這意味著操作的結(jié)果是一個(gè)類型的對(duì)象。反之,如果對(duì)象存在,這次調(diào)用就會(huì)將其作為函數(shù)的輸入,并按照與方法的約定返回一個(gè)對(duì)象。 一、Optional 類入門 Java 8中引入了一個(gè)新的類java.util.Optional。變量存在時(shí),Optional類只是對(duì)類簡(jiǎn)單封裝。變量不存在時(shí),缺失的值會(huì)被建模成一個(gè)空的Optional對(duì)象,由方法Optional.empt...
摘要:跳過元素流還支持方法,返回一個(gè)扔掉了前個(gè)元素的流。歸約到目前為止,我們見到過的終端操作都是返回一個(gè)之類的或?qū)ο蟮取_@樣的查詢可以被歸類為歸約操作將流歸約成一個(gè)值。通過反復(fù)使用加法,你把一個(gè)數(shù)字列表歸約成了一個(gè)數(shù)字。 使用流 在上一篇的讀書筆記中,我們已經(jīng)看到了流讓你從外部迭代轉(zhuǎn)向內(nèi)部迭代。這樣,你就用不著寫下面這樣的代碼來顯式地管理數(shù)據(jù)集合的迭代(外部迭代)了: /** * 菜單 ...
摘要:收集器用作高級(jí)歸約剛剛的結(jié)論又引出了優(yōu)秀的函數(shù)式設(shè)計(jì)的另一個(gè)好處更易復(fù)合和重用。更具體地說,對(duì)流調(diào)用方法將對(duì)流中的元素觸發(fā)一個(gè)歸約操作由來參數(shù)化。另一個(gè)常見的返回單個(gè)值的歸約操作是對(duì)流中對(duì)象的一個(gè)數(shù)值字段求和。 用流收集數(shù)據(jù) 我們?cè)谇耙徽轮袑W(xué)到,流可以用類似于數(shù)據(jù)庫的操作幫助你處理集合。你可以把Java 8的流看作花哨又懶惰的數(shù)據(jù)集迭代器。它們支持兩種類型的操作:中間操作(如 filt...
摘要:第三個(gè)問題查找所有來自于劍橋的交易員,并按姓名排序。第六個(gè)問題打印生活在劍橋的交易員的所有交易額。第八個(gè)問題找到交易額最小的交易。 付諸實(shí)戰(zhàn) 在本節(jié)中,我們會(huì)將迄今學(xué)到的關(guān)于流的知識(shí)付諸實(shí)踐。我們來看一個(gè)不同的領(lǐng)域:執(zhí)行交易的交易員。你的經(jīng)理讓你為八個(gè)查詢找到答案。 找出2011年發(fā)生的所有交易,并按交易額排序(從低到高)。 交易員都在哪些不同的城市工作過? 查找所有來自于劍橋的交易...
摘要:首先我們定義一個(gè)有兩個(gè)不同控制器的然后,我們創(chuàng)建一個(gè)特定的工廠接口來創(chuàng)建新的對(duì)象不需要手動(dòng)的去繼承實(shí)現(xiàn)該工廠接口,我們只需要將控制器的引用傳遞給該接口對(duì)象就好了的控制器會(huì)自動(dòng)選擇合適的構(gòu)造器方法。這種指向時(shí)間軸的對(duì)象即是類。 本文為翻譯文章,原文地址 這里 歡迎來到本人對(duì)于Java 8的系列介紹教程,本教程會(huì)引導(dǎo)你一步步領(lǐng)略最新的語法特性。通過一些簡(jiǎn)單的代碼示例你即可以學(xué)到默認(rèn)的接口方...
閱讀 768·2021-11-23 09:51
閱讀 3645·2021-11-15 11:38
閱讀 1027·2021-10-14 09:42
閱讀 3296·2021-09-29 09:35
閱讀 2204·2021-09-03 10:33
閱讀 817·2021-07-30 16:33
閱讀 1613·2019-08-30 15:55
閱讀 1901·2019-08-30 14:04