Java 8 - Optional全解
文章目錄
- 在Optional出現之前經常遇到的空指針異常
- 采用防御式減少NullPointerException (深度質疑)
- null-安全的第二種嘗試(過多的退出語句)
 
- Optional的介紹以及API的詳解
- 使用Optional 重新定義數據模型
- 如何使用 Optional
- 創建Optional對象
- 1.聲明一個空的Optional
- 2.使用一個非空創建Optional
- 3.可接受null值的Optional
 
- 使用map從Optional對象中提取和轉換值
- 使用flatMap鏈調用Optional對象
- 使用 Optional 獲取car的保險名
- 使用 Optional 解引用串接的 Person / Car / Insurance 對象
 
- 默認行為及解引用Optional對象
- get()
- orElse(T other)
- orElseGet(Supplier<? extends T> other)
- orElseThrow(Supplier<? extends X> exceptionSupplier)
- ifPresent(Consumer<? super T>)
 
- 兩個Optional對象的組合
- 使用filter刪除特定的值
 
 
- Optional類的方法
在Optional出現之前經常遇到的空指針異常
NullPointerException 這個你總不能說你沒有碰到過吧
【Person / Car / Insurance 的數據模型】
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 (深度質疑)
怎樣做才能避免這種不期而至的 NullPointerException 呢?通常,你可以在需要的地方添加 null 的檢查(過于激進的防御式檢查甚至會在不太需要的地方添加檢測代碼),并且添加的方式往往各有不同。
下面這個例子是我們試圖在方法中避免 NullPointerException 的第一次嘗試
這個方法每次引用一個變量都會做一次 null 檢查,如果引用鏈上的任何一個遍歷的解變量
 值為 null ,它就返回一個值為“Unknown”的字符串。
每次你不確定一個變量是否為 null 時,都需要添加一個進一步嵌套的 if 塊,也增加了代碼縮進的層數。很明顯,這種方式不具備擴展性,同時還降低了代碼的可讀性。
面對這種情況,你也許愿意嘗試另一種方案。下面的代碼清單中,我們試圖通過一種不同的方式避免這種問題。
null-安全的第二種嘗試(過多的退出語句)
為了避免深層遞歸的 if 語句塊,采用了一種不同的策略: 每次遇到null, 都返回一個unknown常量。
然而,這種方案遠非理想,現在這個方法有了四個不同的退出點,使得代碼的維護異常困難。
更糟糕的是,發生 null 時返回的默認值,即字符串“Unknown”在三個不同的地方重復出現——出現拼寫寫錯誤的概率不小!當然,你可能會說,我們可以用把它們抽取到一個常量中的方式避免這種問題。
進一步而言,這種流程是極易出錯的;如果你忘記檢查了那個可能為 null 的屬性會怎樣?
使用 null 來表示變量值的缺失是大錯特錯的。你需要更優雅的方式來對缺失的變量值建模。
Optional的介紹以及API的詳解
Java 8中引入了一個新的類 java.util.Optional<T> 。這是一個封裝 Optional 值的類。
舉例來說,使用新的類意味著,如果你知道一個人可能有也可能沒有車,那么 Person 類內部的 car 變量就不應該聲明為 Car ,遭遇某人沒有車時把 null 引用賦值給它,而是應該像下圖那樣直接將其聲明為 Optional<Car> 類型。
變量存在時, Optional 類只是對類簡單封裝。變量不存在時,缺失的值會被建模成一個“空”的 Optional 對象,由方法 Optional.empty() 返回
Optional.empty() 方法是一個靜態工廠,方法,它返回 Optional 類的特定單一實例。
null VS Optional.empty
引 用 一 個 null , 一 定 會 觸 發 NullPointerException , 不 過 使 用Optional.empty() 就完全沒事兒,它是 Optional 類的一個有效對象,多種場景都能調用,非常有用.
使用 Optional 而不是 null 的一個非常重要而又實際的語義區別是,第一個例子中,我們在聲明變量時使用的是 Optional<Car> 類型,而不是 Car 類型,這句聲明非常清楚地表明了這里發生變量缺失是允許的。
public class Person {private Optional<Car> car;public Optional<Car> getCar() {return this.car;} } public class Car {private Optional<Insurance> insurance;public Optional<Insurance> getInsurance() {return insurance;} }與此相反,使用 Car 這樣的類型,可能將變量賦值為 null ,這意味著你需要獨立面對這些,你只能依賴你對業務模型的理解,判斷一個 null 是否屬于該變量的有效范圍。
使用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
創建Optional對象
使用 Optional 之前,你首先需要學習的是如何創建 Optional 對象。完成這一任務有多種方法。
1.聲明一個空的Optional
可以通過靜態工廠方法 Optional.empty ,創建一個空的 Optional對象
Optional<Car> car = Optional.empty();2.使用一個非空創建Optional
還可以使用靜態工廠方法 Optional.of ,依據一個非空值創建一個 Optional 對象:
Car car1 = new Car();Optional<Car> o = Optional.of(car1);如果 car 是一個 null ,這段代碼會立即?出一個 NullPointerException ,而不是等到你試圖訪問 car 的屬性值時才返回一個錯誤。
Optional<Car> o = Optional.of(null);
 
3.可接受null值的Optional
最后,使用靜態工廠方法 Optional.ofNullable ,你可以創建一個允許 null 值的 Optional對象
Car car1 = new Car();Optional<Car> o = Optional.ofNullable(car1); System.out.println(o);Optional<Car> o2 = Optional.ofNullable(null); System.out.println(o2);如果 car 是 null ,那么得到的 Optional 對象就是個空對象。
 
創建完成了, 我們還需要繼續研究“如何獲取 Optional 變量中的值”。尤其是, Optional提供了一個 get 方法,它能非常精準地完成這項工作,我們在后面會詳細介紹這部分內容。
不過get 方法在遇到空的 Optional 對象時也會拋出異常,所以不按照約定的方式使用它,又會讓我們再度陷入由 null 引起的代碼維護的夢魘。因此,我們首先從無需顯式檢查的 Optional 值的使用入手,這些方法與 Stream 中的某些操作極其相似。
使用map從Optional對象中提取和轉換值
從對象中提取信息是一種比較常見的模式。比如,你可能想要從 insurance 對象中提取名稱。提取名稱之前,你需要檢查 insurance 對象是否為 null 。
代碼如下所示
String name = null; if(insurance != null){name = insurance.getName(); }為了支持這種模式, Optional 提供了一個 map 方法。它的工作方式如下
Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);optionalInsurance.map(Insurance::getName);從概念上,這與我們之前看到的流的 map 方法相差無幾。 map 操作會將提供的函數應用于流的每個元素。
你可以把 Optional 對象看成一種特殊的集合數據,它至多包含一個元素。如果 Optional 包含一個值,那函數就將該值作為參數傳遞給 map ,對該值進行轉換。如果 Optional 為空,就什么也不做。
下圖對這種相似性進行了說明,展示了把一個將正方形轉換為三角形的函數,分別傳遞給正方形和 Optional 正方形流的 map 方法之后的結果。
這看起來挺有用,但是你怎樣才能應用起來,重構之前的代碼呢?前文的代碼里用安全的方式鏈接了多個方法。
public String getCarInsuranceName(Person person) {return person.getCar().getInsurance().getName(); }為了達到這個目的,我們需要求助 Optional 提供的另一個方法 flatMap
使用flatMap鏈調用Optional對象
由于我們剛剛學習了如何使用 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 方法。
那遭遇到了的嵌套式 optional結構,該如何解決這個問題呢
讓我們再回顧一下在流上使用過的模式:
flatMap 方法。使用流時, flatMap 方法接受一個函數作為參數,這個函數的返回值是另一個流。這個方法會應用到流中的每一個元素,最終形成一個新的流的流。
但是 flagMap 會用流的內容替換每個新生成的流。換句話說,由方法生成的各個流會被合并或者扁平化為一個單一的流
這里你希望的結果其實也是類似的,但是你想要的是將兩層的 optional 合并為一個。
這個例子中,傳遞給流的 flatMap 方法會將每個正方形轉換為另一個流中的兩個三角形。那么, map 操作的結果就包含有三個新的流,每一個流包含兩個三角形,但 flatMap 方法會將這種兩層的流合并為一個包含6個三角形的單一流。
類似地,傳遞給 optional 的 flatMap 方法的函數會將原始包含正方形的 optional 對象轉換為包含三角形的 optional 對象。如果將該方法傳遞給 map 方法,結果會是一個 Optional 對象,而這個 Optional 對象中包含了三角形;但 flatMap方法會將這種兩層的 Optional 對象轉換為包含三角形的單一 Optional 對象。
使用 Optional 獲取car的保險名
我們來比較一下之前的寫法
可以看到,處理潛在可能缺失的值時,使用 Optional 具有明顯的優勢。這一次,你可以用非常容易卻又普適的方法實現之前你期望的效果——不再需要使用那么多的條件分支,也不會增加代碼的復雜性
從具體的代碼實現來看,首先我們注意到修改了代碼getCarInsuranceName 方法的簽名,因為我們很明確地知道存在這樣的用例,即一個不存在的Person 被傳遞給了方法,比如, Person 是使用某個標識符從數據庫中查詢出來的,你想要對數庫中不存在指定標識符對應的用戶數據的情況進行建模。你可以將方法的參數類型由 Person改為 Optional<Person> ,對這種特殊情況進行建模。
我們再一次看到這種方式的優點,它通過類型系統讓你的域模型中隱藏的知識顯式地體現在你的代碼中,換句話說,你永遠都不應該忘記語言的首要功能就是溝通,即使對程序設計語言而言也沒有什么不同。
聲明方法接受一個 Optional 參數,或者將結果作為 Optional 類型返回,讓你的同事或者未來你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一個空值
使用 Optional 解引用串接的 Person / Car / Insurance 對象
由 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 代表的值。
接下來我們一起看看吧。
默認行為及解引用Optional對象
orElse 方法讀取這個變量的值,使用這種方式你還可以定義一個默認值,遇空的 Optional 變量時,默認值會作為該方法的調用返回值。
Optional 類提供了多種方法讀取Optional 實例中的變量值
get()
get() 是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接返回封裝的變量
 值,否則就拋出一個 NoSuchElementException 異常。
所以,除非你非常確定 Optional變量一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對于嵌套式的 null檢查,也并未體現出多大的改進。
Optional<Person> optionalPerson = Optional.ofNullable(null);System.out.println(optionalPerson.get());orElse(T other)
它允許你在Optional 對象不包含值時提供一個默認值.
Optional<String> optional = Optional.ofNullable("artisan");Optional<String> optional2 = Optional.ofNullable(null);System.out.println(optional.orElse("default value"));System.out.println(optional2.orElse("default value"));orElseGet(Supplier<? extends T> other)
是 orElse 方法的延遲調用版, Supplier方法只有在 Optional 對象不含值時才執行調用。如果創建默認值是件費時費力的工作你應該考慮采用這種方式(借此提升程序的性能),或者你需要非常確定某個方法僅在Optional 為空時才進行調用,也可以考慮該方式(這種情況有嚴格的限制條件)。
Optional<Person> optionalPerson = Optional.ofNullable(null);//如果為空,則執行后面的邏輯System.out.println(optionalPerson.orElseGet(Person::new));System.out.println(optionalPerson.orElseGet(() -> new Person()));orElseThrow(Supplier<? extends X> exceptionSupplier)
和 get 方法非常類似,它們遇 Optional 對象為空時都會拋出一個異常,但是使用 orElseThrow 你可以定制希望拋出的異常類型
Optional<Person> optionalPerson = Optional.ofNullable(null);//如果為空,則執行后面的邏輯 拋出異常System.out.println(optionalPerson.orElseThrow(RuntimeException::new));Optional<Person> optionalPerson2 = Optional.ofNullable(null);//如果為空,則執行后面的邏輯 拋出自定義異常System.out.println(optionalPerson2.orElseThrow(()->new RuntimeException("null 啦。。。。")));ifPresent(Consumer<? super T>)
讓你能在變量值存在時執行一個作為參數傳入的方法,否則就不進行任何操作.
Optional<String> stringOptional = Optional.ofNullable("artisan");// 如果存在 這執行打印stringOptional.ifPresent(System.out::println);Optional<String> stringOptional2 = Optional.ofNullable(null);// 如果存在 這執行打印stringOptional2.ifPresent(System.out::println);可以看到第二個并沒有輸出,因為 為null ,不滿足條件
 
兩個Optional對象的組合
現在,我們假設你有這樣一個方法,它接受一個 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 對象】
結合 map 和 flatMap 方法,用一行代碼重新實現之前出現的 nullSafeFind-CheapestInsurance() 方法。 你可以像使用三元操作符元那樣,無需任何判斷條件的的結構,以一行代碼實現該方法
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); }這段代碼中,你對第一個 Optional 對象調用 flatMap 方法,如果它是個空值,傳遞給它的Lambda表達式不會執行,這次調用會直接返回一個空的 Optional 對象。
反之,如果 person對象存在,這次調用就會將其作為函數 Function 的輸入,并調用flatMap 方法的約定返回一個 Optional<Insurance> 對象。
這個函數的函數體會對第二個 Optional 對象執行 map 操作,如果第二個對象不包含 car ,函數 Function 就返回一個空的 Optional 對象,整個nullSafeFindCheapestInsuranc 方法的返回值也是一個空的 Optional 對象。
最后,如果person 和 car 對象都存在,作為參數傳遞給 map 方法的Lambda表達式能夠使用這兩個值安全地調用用原始的 findCheapestInsurance 方法,完成期望的操作
使用filter刪除特定的值
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 對象進行過濾】
假設在我們的 Person / Car / Insurance 模型中, Person 還提供了一個方法可以取得Person 對象的年齡。
請使用下面的簽名改寫上面流程中的 getCarInsuranceName 方法:
public String getCarInsuranceName(Optional<Person> person, int minAge)找出年齡大于或者等于 minAge 參數的 Person 所對應的保險公司列表
分析: 你可以對 Optional 封裝的 Person 對象進行 filter 操作, 設置相應的條件謂詞,
即如果 person 的年齡大于 minAge 參數的設定值值,就返回該值,并將謂詞傳遞給 filter 方法
Optional類的方法
 
 
總結
以上是生活随笔為你收集整理的Java 8 - Optional全解的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Java 8 - 收集器Collecto
- 下一篇: Java 8 - Completable
