若是你做爲Java程序員曾經遭遇過NullPointerException,請舉起手。若是這是你最常遭遇的異常,請繼續舉手。很是惋惜,這個時刻,咱們沒法看到對方,可是我相信不少人的手這個時刻是舉着的。咱們還猜測你可能也有這樣的想法:「毫無疑問,我認可,對任何一位Java程序員來講,不管是初出茅廬的新人,仍是久經江湖的專家,NullPointerException都是他心中的痛,但是咱們又無能爲力,由於這就是咱們爲了使用方便甚至不可避免的像null引用這樣的構造所付出的代價。」這就是程序設計世界裏你們都持有的觀點,然而,這可能並不是事實的所有真相,只是咱們根深蒂固的一種偏見。java
1965年,英國一位名爲Tony Hoare的計算機科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的類型語言之一。Hoare選擇null引用這種方式,「只是由於這種方法實現起來很是容易」。雖然他的設計初衷就是要「經過編譯器的自動檢測機制,確保全部使用引用的地方都是絕對安全的」,他仍是決定爲null引用開個綠燈,由於他認爲這是爲「不存在的值」建模最容易的方式。不少年後,他開始爲本身曾經作過這樣的決定然後悔不迭,把它稱爲「我價值百萬的重大失誤」。咱們已經看到它帶來的後果——程序員對對象的字段進行檢查,判斷它的值是否爲指望的格式,最終卻發現咱們查看的並非一個對象,而是一個空指針,它會當即拋出一個讓人厭煩的NullPointerException異常。git
實際上,Hoare的這段話低估了過去五十年來數百萬程序員爲修復空引用所耗費的代價。近十年出現的大多數現代程序設計語言,包括Java,都採用了一樣的設計方式,其緣由是爲了與更老的語言保持兼容,或者就像Hoare曾經陳述的那樣,「僅僅是由於這樣實現起來更加容易」。讓咱們從一個簡單的例子入手,看看使用null都有什麼樣的問題。程序員
假設你須要處理下面這樣的嵌套對象,這是一個擁有汽車及汽車保險的客戶。github
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(); }
這段代碼看起來至關正常,可是現實生活中不少人沒有車。因此調用getCar方法的結果會怎樣呢?在實踐中,一種比較常見的作法是返回一個null引用,表示該值的缺失,即用戶沒有車。而接下來,對getInsurance的調用會返回null引用的insurance,這會致使運行時出現一個NullPointerException,終止程序的運行。但這還不是所有。若是返回的person值爲null會怎樣?若是getInsurance的返回值也是null,結果又會怎樣?安全
怎樣作才能避免這種不期而至的NullPointerException呢?一般,你能夠在須要的地方添加null的檢查(過於激進的防護式檢查甚至會在不太須要的地方添加檢測代碼),而且添加的方式每每各有不一樣。下面這個例子是咱們試圖在方法中避免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"; }
這個方法每次引用一個變量都會作一次null檢查,若是引用鏈上的任何一個遍歷的解變量值爲null,它就返回一個值爲「Unknown」的字符串。惟一的例外是保險公司的名字,你不須要對它進行檢查,緣由很簡單,由於任何一家公司一定有個名字。注意到了嗎,因爲你掌握業務領域的知識,避免了最後這個檢查,但這並不會直接反映在你建模數據的Java類之中。工具
咱們將上面的代碼標記爲「深層質疑」,緣由是它不斷重複着一種模式:每次你不肯定一個變量是否爲null時,都須要添加一個進一步嵌套的if塊,也增長了代碼縮進的層數。很明顯,這種方式不具有擴展性,同時還犧牲了代碼的可讀性。面對這種窘境,你也許願意嘗試另外一種方案。下面的代碼清單中,咱們試圖經過一種不一樣的方式避免這種問題。性能
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變量,都返回一個字符串常量「Unknown」。然而,這種方案遠非理想,如今這個方法有了四個大相徑庭的退出點,使得代碼的維護異常艱難。更糟的是,發生null時返回的默認值,即字符串「Unknown」在三個不一樣的地方重複出現——出現拼寫錯誤的機率不小!固然,你可能會說,咱們能夠用把它們抽取到一個常量中的方式避免這種問題。學習
進一步而言,這種流程是極易出錯的;若是你忘記檢查了那個可能爲null的屬性會怎樣?經過這一章的學習,你會了解使用null來表示變量值的缺失是大錯特錯的。你須要更優雅的方式來對缺失的變量值建模。
讓咱們一塊兒回顧一下到目前爲止進行的討論,在Java程序開發中使用null會帶來理論和實際操做上的種種問題。
爲了更好的解決和避免NPE異常,Java 8中引入了一個新的類java.util.Optional<T>。這是一個封裝Optional值的類。舉例來講,使用新的類意味着,若是你知道一我的可能有也可能沒有車,那麼Person類內部的car變量就不該該聲明爲Car,遭遇某人沒有車時把null引用賦值給它,而是將其聲明爲Optional<Car>類型。
變量存在時,Optional類只是對類簡單封裝。變量不存在時,缺失的值會被建模成一個「空」的Optional對象,由方法Optional.empty()返回。Optional.empty()方法是一個靜態工廠方法,它返回Optional類的特定單一實例。你可能還有疑惑,null引用和Optional.empty()有什麼本質的區別嗎?從語義上,你能夠把它們看成一回事兒,可是實際中它們之間的差異很是大: 若是你嘗試解引用一個null , 必定會觸發NullPointerException , 不過使用Optional.empty()就徹底沒事兒,它是Optional類的一個有效對象,多種場景都能調用,很是有用。關於這一點,接下來的部分會詳細介紹。
使用Optional而不是null的一個很是重要而又實際的語義區別是,第一個例子中,咱們在聲明變量時使用的是Optional<Car>類型,而不是Car類型,這句聲明很是清楚地代表了這裏發生變量缺失是容許的。與此相反,使用Car這樣的類型,可能將變量賦值爲null,這意味着你須要獨立面對這些,你只能依賴你對業務模型的理解,判斷一個null是否屬於該變量的有效範疇。
牢記上面這些原則,你如今可使用Optional類對最初的代碼進行重構,結果以下。
public class Person { private Optional<Car> car; public Optional<Car> getCar() { return car; } } public class Insurance { private String name; public String getName() { return name; } } public class Car { private Optional<Insurance> insurance; public Optional<Insurance> getInsurance() { return insurance; } }
發現Optional是如何豐富你模型的語義了吧。代碼中person引用的是Optional<Car>,而car引用的是Optional<Insurance>,這種方式很是清晰地表達了你的模型中一個person可能擁有也可能沒有car的情形,一樣,car可能進行了保險,也可能沒有保險。
與此同時,咱們看到insurance公司的名稱被聲明成String類型,而不是Optional<String>,這很是清楚地代表聲明爲insurance公司的類型必須提供公司名稱。使用這種方式,一旦解引用insurance公司名稱時發生NullPointerException,你就能很是肯定地知道出錯的緣由,再也不須要爲其添加null的檢查,由於null的檢查只會掩蓋問題,並未真正地修復問題。insurance公司必須有個名字,因此,若是你遇到一個公司沒有名稱,你須要調查你的數據出了什麼問題,而不該該再添加一段代碼,將這個問題隱藏。
在你的代碼中始終如一地使用Optional,能很是清晰地界定出變量值的缺失是結構上的問題,仍是你算法上的缺陷,抑或是你數據中的問題。另外,咱們還想特別強調,引入Optional類的意圖並不是要消除每個null引用。與此相反,它的目標是幫助你更好地設計出普適的API,讓程序員看到方法簽名,就能瞭解它是否接受一個Optional的值。這種強制會讓你更積極地將變量從Optional中解包出來,直面缺失的變量值。
到目前爲止,一切都很順利;你已經知道了如何使用Optional類型來聲明你的域模型,也瞭解了這種方式與直接使用null引用表示變量值的缺失的優劣。可是,咱們該如何使用呢?用這種方式能作什麼,或者怎樣使用Optional封裝的值呢?
使用Optional以前,你首先須要學習的是如何建立Optional對象。完成這一任務有多種方法。
正如前文已經提到,你能夠經過靜態工廠方法Optional.empty,建立一個空的Optional對象:
Optional<Car> optCar = Optional.empty();
你還可使用靜態工廠方法Optional.of,依據一個非空值建立一個Optional對象:
Optional<Car> optCar = Optional.of(car);
若是car是一個null,這段代碼會當即拋出一個NullPointerException,而不是等到你試圖訪問car的屬性值時才返回一個錯誤。
最後,使用靜態工廠方法Optional.ofNullable,你能夠建立一個容許null值的Optional對象:
Optional<Car> optCar = Optional.ofNullable(car);
若是car是null,那麼獲得的Optional對象就是個空對象。
你可能已經猜到,咱們還須要繼續研究「如何獲取Optional變量中的值」。尤爲是,Optional提供了一個get方法,它能很是精準地完成這項工做,咱們在後面會詳細介紹這部份內容。不過get方法在遭遇到空的Optional對象時也會拋出異常,因此不按照約定的方式使用它,又會讓咱們再度陷入由null引發的代碼維護的夢魘。所以,咱們首先從無需顯式檢查的Optional值的使用入手,這些方法與Stream中的某些操做極其類似。
從對象中提取信息是一種比較常見的模式。好比,你可能想要從insurance公司對象中提取公司的名稱。提取名稱以前,你須要檢查insurance對象是否爲null,代碼以下所示:
String name = null; if(insurance != null){ name = insurance.getName(); }
爲了支持這種模式,Optional提供了一個map方法。它的工做方式以下:
Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance); Optional<String> name = optionalInsurance.map(Insurance::getName);
從概念上,這與咱們在第4章和第5章中看到的流的map方法相差無幾。map操做會將提供的函數應用於流的每一個元素。你能夠把Optional對象當作一種特殊的集合數據,它至多包含一個元素。若是Optional包含一個值,那函數就將該值做爲參數傳遞給map,對該值進行轉換。若是Optional爲空,就什麼也不作。
這看起來挺有用,可是你怎樣才能應用起來,重構以前的代碼呢?前文的代碼裏用安全的方式連接了多個方法。
public String getCarInsuranceName(Person person) { return person.getCar().getInsurance().getName(); }
爲了達到這個目的,咱們須要求助Optional提供的另外一個方法flatMap。
因爲咱們剛剛學習瞭如何使用map,你的第一反應多是咱們能夠利用map重寫以前的代碼,以下所示:
Optional<Person> optPerson = Optional.of(person); Optional<String> name = optPerson.map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName);
不幸的是,這段代碼沒法經過編譯。爲何呢?optPerson是Optional<Person>類型的變量, 調用map方法應該沒有問題。但getCar返回的是一個Optional<Car>類型的對象,這意味着map操做的結果是一個Optional<Optional<Car>>類型的對象。所以,它對getInsurance的調用是非法的,由於最外層的optional對象包含了另外一個optional對象的值,而它固然不會支持getInsurance方法。
因此,咱們該如何解決這個問題呢?讓咱們再回顧一下你剛剛在流上使用過的模式:flatMap方法。使用流時,flatMap方法接受一個函數做爲參數,這個函數的返回值是另外一個流。這個方法會應用到流中的每個元素,最終造成一個新的流的流。可是flagMap會用流的內容替換每一個新生成的流。換句話說,由方法生成的各個流會被合併或者扁平化爲一個單一的流。這裏你但願的結果其實也是相似的,可是你想要的是將兩層的optional合併爲一個。
這個例子中,傳遞給流的flatMap方法會將每一個正方形轉換爲另外一個流中的兩個三角形。那麼,map操做的結果就包含有三個新的流,每個流包含兩個三角形,但flatMap方法會將這種兩層的流合併爲一個包含六個三角形的單一流。相似地,傳遞給optional的flatMap方法的函數會將原始包含正方形的optional對象轉換爲包含三角形的optional對象。若是將該方法傳遞給map方法,結果會是一個Optional對象,而這個Optional對象中包含了三角形;但flatMap方法會將這種兩層的Optional對象轉換爲包含三角形的單一Optional對象。
相信如今你已經對Optional的map和flatMap方法有了必定的瞭解,讓咱們看看如何應用。
public String getCarInsuranceName(Optional<Person> person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) // 若是Optional的j結果爲空值,設置默認值 .orElse("Unknown"); }
咱們能夠看到,處理潛在可能缺失的值時,使用Optional具備明顯的優點。這一次,你能夠用很是容易卻又普適的方法實現以前你指望的效果——再也不須要使用那麼多的條件分支,也不會增長代碼的複雜性。
由Optional<Person>對象,咱們能夠結合使用以前介紹的map和flatMap方法,從Person中解引用出Car,從Car中解引用出Insurance,從Insurance對象中解引用出包含insurance公司名稱的字符串。
這裏,咱們從以Optional封裝的Person入手,對其調用flatMap(Person::getCar)。如前所述,這種調用邏輯上能夠劃分爲兩步。第一步,某個Function做爲參數,被傳遞給由Optional封裝的Person對象,對其進行轉換。這個場景中,Function的具體表現是一個方法引用,即對Person對象的getCar方法進行調用。因爲該方法返回一個Optional<Car>類型的對象,Optional內的Person也被轉換成了這種對象的實例,結果就是一個兩層的Optional對象,最終它們會被flagMap操做合併。從純理論的角度而言,你能夠將這種合併操做簡單地當作把兩個Optional對象結合在一塊兒,若是其中有一個對象爲空,就構成一個空的Optional對象。若是你對一個空的Optional對象調用flatMap,實際狀況又會如何呢?結果不會發生任何改變,返回值也是個空的Optional對象。與此相反,若是Optional封裝了一個Person對象,傳遞給flapMap的Function,就會應用到Person上對其進行處理。這個例子中,因爲Function的返回值已是一個Optional對象,flapMap方法就直接將其返回。
第二步與第一步大同小異,它會將Optional<Car>轉換爲Optional<Insurance>。第三步則會將Optional<Insurance>轉化爲Optional<String>對象,因爲Insurance.getName()方法的返回類型爲String,這裏就再也不須要進行flapMap操做了。
截至目前爲止,返回的Optional多是兩種狀況:若是調用鏈上的任何一個方法返回一個空的Optional,那麼結果就爲空,不然返回的值就是你指望的保險公司的名稱。那麼,你如何讀出這個值呢?畢竟你最後獲得的這個對象仍是個Optional<String>,它可能包含保險公司的名稱,也可能爲空。咱們使用了一個名爲orElse的方法,當Optional的值爲空時,它會爲其設定一個默認值。除此以外,還有不少其餘的方法能夠爲Optional設定默認值,或者解析出Optional表明的值。接下來咱們會對此作進一步的探討。
咱們決定採用orElse方法讀取這個變量的值,使用這種方式你還能夠定義一個默認值,遭遇空的Optional變量時,默認值會做爲該方法的調用返回值。Optional類提供了多種方法讀取Optional實例中的變量值。
Optional類和Stream接口的類似之處,遠不止map和flatMap這兩個方法。還有第三個方法filter,它的行爲在兩種類型之間也極其類似。
如今,咱們假設你有這樣一個方法,它接受一個Person和一個Car對象,並以此爲條件對外部提供的服務進行查詢,經過一些複雜的業務邏輯,試圖找到知足該組合的最便宜的保險公司:
public Insurance findCheapestInsurance(Person person, Car car) { // 不一樣的保險公司提供的查詢服務 // 對比全部數據 return cheapestCompany; }
咱們還假設你想要該方法的一個null-安全的版本,它接受兩個Optional對象做爲參數,返回值是一個Optional<Insurance>對象,若是傳入的任何一個參數值爲空,它的返回值亦爲空。Optional類還提供了一個isPresent方法,若是Optional對象包含值,該方法就返回true,因此你的第一想法多是經過下面這種方式實現該方法:
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(); } }
這個方法具備明顯的優點,咱們從它的簽名就能很是清楚地知道不管是person仍是car,它的值都有可能爲空,出現這種狀況時,方法的返回值也不會包含任何值。不幸的是,該方法的具體實現和你以前曾經實現的null檢查太類似了:方法接受一個Person和一個Car對象做爲參數,而兩者都有可能爲null。利用Optional類提供的特性,有沒有更好或更地道的方式來實現這個方法呢?
Optional類和Stream接口的類似之處遠不止map和flatMap這兩個方法。還有第三個方法filter,它的行爲在兩種類型之間也極其類似,咱們在接下來的一節會進行介紹。
你常常須要調用某個對象的方法,查看它的某些屬性。好比,你可能須要檢查保險公司的名稱是否爲「Cambridge-Insurance」。爲了以一種安全的方式進行這些操做,你首先須要肯定引用指向的Insurance對象是否爲null,以後再調用它的getName方法,以下所示:
Insurance insurance = ...; if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ System.out.println("ok"); }
使用Optional對象的filter方法,這段代碼能夠重構以下:
Optional<Insurance> optInsurance = ...; optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) .ifPresent(x -> System.out.println("ok"));
filter方法接受一個謂詞做爲參數。若是Optional對象的值存在,而且它符合謂詞的條件,filter方法就返回其值;不然它就返回一個空的Optional對象。若是你還記得咱們能夠將Optional當作最多包含一個元素的Stream對象,這個方法的行爲就很是清晰了。若是Optional對象爲空,它不作任何操做,反之,它就對Optional對象中包含的值施加謂詞操做。若是該操做的結果爲true,它不作任何改變,直接返回該Optional對象,不然就將該值過濾掉,將Optional的值置空。
下一節中,咱們會探討Optional類剩下的一些特性,並提供更實際的例子,展現多種你可以應用於代碼中更好地管理缺失值的技巧。
相信你已經瞭解,有效地使用Optional類意味着你須要對如何處理潛在缺失值進行全面的反思。這種反思不只僅限於你曾經寫過的代碼,更重要的多是,你如何與原生Java API實現共存雙贏。
實際上,咱們相信若是Optional類可以在這些API建立之初就存在的話,不少API的設計編寫可能會大有不一樣。爲了保持後向兼容性,咱們很難對老的Java API進行改動,讓它們也使用Optional,但這並不表示咱們什麼也作不了。你能夠在本身的代碼中添加一些工具方法,修復或者繞過這些問題,讓你的代碼能享受Optional帶來的威力。咱們會經過幾個實際的例子講解如何達到這樣的目的。
現存Java API幾乎都是經過返回一個null的方式來表示須要值的缺失,或者因爲某些緣由計算沒法獲得該值。好比,若是Map中不含指定的鍵對應的值,它的get方法會返回一個null。可是,正如咱們以前介紹的,大多數狀況下,你可能但願這些方法能返回一個Optional對象。你沒法修改這些方法的簽名,可是你很容易用Optional對這些方法的返回值進行封裝。咱們接着用Map作例子,假設你有一個Map<String, Object>方法,訪問由key索引的值時,若是map中沒有與key關聯的值,該次調用就會返回一個null。
Object value = map.get("key");
使用Optional封裝map的返回值,你能夠對這段代碼進行優化。要達到這個目的有兩種方式:你可使用笨拙的if-then-else判斷語句,毫無疑問這種方式會增長代碼的複雜度;或者你能夠採用咱們前文介紹的Optional.ofNullable方法:
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你但願安全地對潛在爲null的對象進行轉換,將其替換爲Optional對象時,均可以考慮使用這種方法。
因爲某種緣由,函數沒法返回某個值,這時除了返回null,Java API比較常見的替代作法是拋出一個異常。這種狀況比較典型的例子是使用靜態方法Integer.parseInt(String),將String轉換爲int。在這個例子中,若是String沒法解析到對應的整型,該方法就拋出一個NumberFormatException。最後的效果是,發生String沒法轉換爲int時,代碼發出一個遭遇非法參數的信號,惟一的不一樣是,此次你須要使用try/catch 語句,而不是使用if條件判斷來控制一個變量的值是否非空。
你也能夠用空的Optional對象,對遭遇沒法轉換的String時返回的非法值進行建模,這時你指望parseInt的返回值是一個optional。咱們沒法修改最初的Java方法,可是這無礙咱們進行須要的改進,你能夠實現一個工具方法,將這部分邏輯封裝於其中,最終返回一個咱們但願的Optional對象,代碼以下所示。
public static Optional<Integer> stringToInt(String s) { try { return Optional.of(Integer.parseInt(s)); } catch (NumberFormatException e) { return Optional.empty(); } }
咱們的建議是,你能夠將多個相似的方法封裝到一個工具類中,讓咱們稱之爲OptionalUtility。經過這種OptionalUtility.stringToInt方法,將String轉換爲一個Optional<Integer>對象,而再也不須要記得你在其中封裝了笨拙的try/catch的邏輯了。
Github: chap10
Gitee: chap10