亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

《Java8實(shí)戰(zhàn)》-第十章筆記(用Optional取代null)

flybywind / 1824人閱讀

摘要:是第一批在堆上分配記錄的類型語言之一。實(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。這是一個(gè)封裝Optional值的類。舉例來說,使用新的類意味著,如果你知道一個(gè)人可能有也可能沒有車,那么Person類內(nèi)部的car變量就不應(yīng)該聲明為Car,遭遇某人沒有車時(shí)把null引用賦值給它,而是將其聲明為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類型,而不是Car類型,這句聲明非常清楚地表明了這里發(fā)生變量缺失是允許的。與此相反,使用Car這樣的類型,可能將變量賦值為null,這意味著你需要獨(dú)立面對(duì)這些,你只能依賴你對(duì)業(yè)務(wù)模型的理解,判斷一個(gè)null是否屬于該變量的有效范疇。

牢記上面這些原則,你現(xiàn)在可以使用Optional類對(duì)最初的代碼進(jìn)行重構(gòu),結(jié)果如下。

public class Person {
    private Optional car;

    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,而car引用的是Optional,這種方式非常清晰地表達(dá)了你的模型中一個(gè)person可能擁有也可能沒有car的情形,同樣,car可能進(jìn)行了保險(xiǎn),也可能沒有保險(xiǎn)。

與此同時(shí),我們看到insurance公司的名稱被聲明成String類型,而不是Optional,這非常清楚地表明聲明為insurance公司的類型必須提供公司名稱。使用這種方式,一旦解引用insurance公司名稱時(shí)發(fā)生NullPointerException,你就能非常確定地知道出錯(cuò)的原因,不再需要為其添加null的檢查,因?yàn)閚ull的檢查只會(huì)掩蓋問題,并未真正地修復(fù)問題。insurance公司必須有個(gè)名字,所以,如果你遇到一個(gè)公司沒有名稱,你需要調(diào)查你的數(shù)據(jù)出了什么問題,而不應(yīng)該再添加一段代碼,將這個(gè)問題隱藏。

在你的代碼中始終如一地使用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ì)象:

Optional optCar = Optional.empty();

依據(jù)一個(gè)非空值創(chuàng)建Optional

你還可以使用靜態(tài)工廠方法Optional.of,依據(jù)一個(gè)非空值創(chuàng)建一個(gè)Optional對(duì)象:

Optional optCar = 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ì)象:

Optional optCar = 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方法。它的工作方式如下:

Optional optionalInsurance = 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重寫之前的代碼,如下所示:

Optional optPerson = Optional.of(person);
Optional name = optPerson.map(Person::getCar)
                    .map(Car::getInsurance)
                    .map(Insurance::getName);

不幸的是,這段代碼無法通過編譯。為什么呢?optPerson是Optional類型的變量, 調(diào)用map方法應(yīng)該沒有問題。但getCar返回的是一個(gè)Optional類型的對(duì)象,這意味著map操作的結(jié)果是一個(gè)Optional>類型的對(duì)象。因此,它對(duì)getInsurance的調(diào)用是非法的,因?yàn)樽钔鈱拥膐ptional對(duì)象包含了另一個(gè)optional對(duì)象的值,而它當(dāng)然不會(huì)支持getInsurance方法。

所以,我們?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(Optional person) {
        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對(duì)象,我們可以結(jié)合使用之前介紹的map和flatMap方法,從Person中解引用出Car,從Car中解引用出Insurance,從Insurance對(duì)象中解引用出包含insurance公司名稱的字符串。

這里,我們從以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類型的對(duì)象,Optional內(nèi)的Person也被轉(zhuǎn)換成了這種對(duì)象的實(shí)例,結(jié)果就是一個(gè)兩層的Optional對(duì)象,最終它們會(huì)被flagMap操作合并。從純理論的角度而言,你可以將這種合并操作簡(jiǎn)單地看成把兩個(gè)Optional對(duì)象結(jié)合在一起,如果其中有一個(gè)對(duì)象為空,就構(gòu)成一個(gè)空的Optional對(duì)象。如果你對(duì)一個(gè)空的Optional對(duì)象調(diào)用flatMap,實(shí)際情況又會(huì)如何呢?結(jié)果不會(huì)發(fā)生任何改變,返回值也是個(gè)空的Optional對(duì)象。與此相反,如果Optional封裝了一個(gè)Person對(duì)象,傳遞給flapMap的Function,就會(huì)應(yīng)用到Person上對(duì)其進(jìn)行處理。這個(gè)例子中,由于Function的返回值已經(jīng)是一個(gè)Optional對(duì)象,flapMap方法就直接將其返回。

第二步與第一步大同小異,它會(huì)將Optional轉(zhuǎn)換為Optional。第三步則會(huì)將Optional轉(zhuǎn)化為Optional對(duì)象,由于Insurance.getName()方法的返回類型為String,這里就不再需要進(jìn)行flapMap操作了。

截至目前為止,返回的Optional可能是兩種情況:如果調(diào)用鏈上的任何一個(gè)方法返回一個(gè)空的Optional,那么結(jié)果就為空,否則返回的值就是你期望的保險(xiǎn)公司的名稱。那么,你如何讀出這個(gè)值呢?畢竟你最后得到的這個(gè)對(duì)象還是個(gè)Optional,它可能包含保險(xiǎn)公司的名稱,也可能為空。我們使用了一個(gè)名為orElse的方法,當(dāng)Optional的值為空時(shí),它會(huì)為其設(shè)定一個(gè)默認(rèn)值。除此之外,還有很多其他的方法可以為Optional設(shè)定默認(rèn)值,或者解析出Optional代表的值。接下來我們會(huì)對(duì)此做進(jìn)一步的探討。

默認(rèn)行為及解引用Optional 對(duì)象

我們決定采用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 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 exceptionSupplier)和get方法非常類似,它們?cè)庥鯫ptional對(duì)象為空時(shí)都會(huì)拋出一個(gè)異常,但是使用orElseThrow你可以定制希望拋出的異常類型。

ifPresent(Consumer)讓你能在變量值存在時(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對(duì)象,如果傳入的任何一個(gè)參數(shù)值為空,它的返回值亦為空。Optional類還提供了一個(gè)isPresent方法,如果Optional對(duì)象包含值,該方法就返回true,所以你的第一想法可能是通過下面這種方式實(shí)現(xiàn)該方法:

public Optional nullSafeFindCheapestInsurance(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)如下:

Optional optInsurance = ...;
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方法,訪問由key索引的值時(shí),如果map中沒有與key關(guān)聯(lián)的值,該次調(diào)用就會(huì)返回一個(gè)null。

Object value = map.get("key");

使用Optional封裝map的返回值,你可以對(duì)這段代碼進(jìn)行優(yōu)化。要達(dá)到這個(gè)目的有兩種方式:你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會(huì)增加代碼的復(fù)雜度;或者你可以采用我們前文介紹的Optional.ofNullable方法:

Optional value = Optional.ofNullable(map.get("key"));

每次你希望安全地對(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 Optional stringToInt(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對(duì)象,而不再需要記得你在其中封裝了笨拙的try/catch的邏輯了。

代碼

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

相關(guān)文章

  • 《java 8 實(shí)戰(zhàn)》讀書筆記 -十章 Optional取代null

    摘要:但返回的是一個(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...

    時(shí)飛 評(píng)論0 收藏0
  • Java8實(shí)戰(zhàn)》-五章讀書筆記(使流Stream-01)

    摘要:跳過元素流還支持方法,返回一個(gè)扔掉了前個(gè)元素的流。歸約到目前為止,我們見到過的終端操作都是返回一個(gè)之類的或?qū)ο蟮取_@樣的查詢可以被歸類為歸約操作將流歸約成一個(gè)值。通過反復(fù)使用加法,你把一個(gè)數(shù)字列表歸約成了一個(gè)數(shù)字。 使用流 在上一篇的讀書筆記中,我們已經(jīng)看到了流讓你從外部迭代轉(zhuǎn)向內(nèi)部迭代。這樣,你就用不著寫下面這樣的代碼來顯式地管理數(shù)據(jù)集合的迭代(外部迭代)了: /** * 菜單 ...

    OldPanda 評(píng)論0 收藏0
  • Java8實(shí)戰(zhàn)》-六章讀書筆記流收集數(shù)據(jù)-01)

    摘要:收集器用作高級(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...

    EscapedDog 評(píng)論0 收藏0
  • Java8實(shí)戰(zhàn)》-五章讀書筆記(使流Stream-02)

    摘要:第三個(gè)問題查找所有來自于劍橋的交易員,并按姓名排序。第六個(gè)問題打印生活在劍橋的交易員的所有交易額。第八個(gè)問題找到交易額最小的交易。 付諸實(shí)戰(zhàn) 在本節(jié)中,我們會(huì)將迄今學(xué)到的關(guān)于流的知識(shí)付諸實(shí)踐。我們來看一個(gè)不同的領(lǐng)域:執(zhí)行交易的交易員。你的經(jīng)理讓你為八個(gè)查詢找到答案。 找出2011年發(fā)生的所有交易,并按交易額排序(從低到高)。 交易員都在哪些不同的城市工作過? 查找所有來自于劍橋的交易...

    liangzai_cool 評(píng)論0 收藏0
  • Java實(shí)戰(zhàn)Java8指南

    摘要:首先我們定義一個(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)的接口方...

    nemo 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<