NullPointerException,大家應該都見過。這是Tony Hoare在設計ALGOL W語言時提出的null引用的想法,他的設計初衷是想通過編譯器的自動檢測機制,確保所有使用引用的地方都是絕對安全的。很多年后,他對自己曾經做過的這個決定而后悔不已,把它稱為“我價值百萬的重大失誤”。它帶來的后果就是---我們想判斷一個對象中的某個字段進行檢查,結果發現我們查看的不是一個對象,而是一個空指針,他會立即拋出NullPointerException異常。
看下面這個例子:
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 p){ return p.getCar().getInsurance().getName(); }
這是一個獲取保險公司名字的方法,但是在庫里可能很多人沒有車,所以會返回null引用。更沒有車險,所以直接返回一個NullPointerException。
為了避免這種情況,我們一般會在需要的地方添加null的檢查,並且添加的方式往往不同。
避免NullPointerException第一次嘗試:
public String getCarInsuranceName(Person p){ if(p != null){ Car car = p.getCar(); if(car != null){ Insurance insurance = car.getInsurance(); if(insurance != null){ return insurance.getName(); } } } return "Unknown"; }
這個方法每次引用一個變量時,都會做一次null檢查,如果任何一個返回值為null,則會返回Unknown。因為知道公司都必須有名字,所以最后一個保險公司的名字沒有進行判斷。這種方式不具備擴展性,同時還犧牲了代碼的可讀性。每一次都要嵌套一個if來進行檢查。
避免NullPointerException第二次嘗試:
public String getCarInsuranceName(Person p) { if (p == null) return "Unknown"; Car car = p.getCar(); if (car == null) return "Unknown"; Insurance insurance = car.getInsurance(); if (insurance == null) return "Unknown"; return insurance.getName(); }
第二種方式,避免了深層if語句塊,采用了每次遇到null都直接返回Unknown字符串的方式。然后這個方案也並非理想,現在這個方法有了四個截然不同的退出點,使代碼的維護更艱難。發生null時的默認值,在三個不同的地方出現,不知道具體是哪個返回null。
Optional類
Java 8中引入了一個新的類java.util.Optional<T>。這是一個封裝Optional值的類。當變量存在時,Optional類知識對類簡單封裝,變量不存在時,缺失的值被建模成一個空的Optional對象,由方法Optional.empty()返回。該方法是一個靜態工廠方法,返回Optional類的特定單一實例。
null和Optional.empty()從語義上,可以當做是一回事。實際上它們之間的差別非常大:如果你嘗試訪問一個null,一定會觸發null引用。而Optional.empty()可以在任何地方訪問。
public class Person { private Optional<Car> car; public Optional<Car> getCar() { return car; } } public class Car { private Optional<Insurance> insurance; public Optional<Insurance> getInsurance() { return insurance; } }
公司的名字我們沒有使用Optional<String> 而是保持了原類型String,那么它就必須設置一個值。
創建Optional對象
1.聲明一個空的Optional
Optional<Car> car = Optional.empty();
2.依據一個非空值創建Optional
Car car = new Car(); Optional<Car> optCar = Optional.of(car);
如果car是null,則直接會報錯null引用,而不是等到你訪問時。
3.可接受null的Optional,這種方式與of不同,編譯器運行時不會報錯。
Car car = null; Optional<Car> optCar = Optional.ofNullable(car);
使用map從Optional對象中提取和轉換值
從對象中讀取信息是一種比較常見的模式。比如,你可以從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);
這里的map和流中的map相差無幾。map操作會將提供的函數應用於流的每個元素。你可以把Optional對象看成一種特殊的集合數據。如圖:
這看齊來挺有用,但是如何應用起來,重構之前的代碼呢?
p.getCar().getInsurance().getName();
使用flatMap鏈接Optional對象
使用剛剛的學習的map,第一反應是重寫之前的代碼,比如這樣:
Optional<Person> person = Optional.of(p); Optional<String> name = person .map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName);
但是這段代碼無法通過編譯,person是Optional<Person>類型的變量,調用map方法沒有問題,但是getCar返回的是一個Optional<Car>類型的對象,這意味着map操作的結果的結果是一個Optional<Optinoal<Car>>類型的對象。 因此它調用getInsurance是非法的。
在流中使用flatMap可以扁平化合並流,在這里你想把兩層的Optional合並為一個。
public String getCarInsuranceName(Person p) { Optional<Person> person = Optional.of(p); return person .flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); }
通過代碼的比較,處理潛在可能缺失的值時,使用Optional具有明顯的優勢。你可以非常容易實現期望的效果,不需要寫那么多的條件分支,也不會增加代碼的復雜性。
首先,Optional.of(p) 生成Optional<person>對象,然后調用person.flatMap(Person::GetCar)返回一個Optional<Car> 對象,Optional內的Person也被轉換成了這種對象,結果就是兩層的Optional對象,最終他們會被flatMap操作合並起來。如果合並時其中有一個為空,那么就構成一個空的Optional對象。如果給一個空的Optional對象調用flatMap返回的也是空的Optional對象。
然后,flatMap(Car::getInsurance) 會轉換成Optional<Insurance> 合並。 第三步 這里調用的是map方法,因為返回類型是string 就不需要flatMap了。如果連上的任何一個結果為空就返回空,否則返回的值就是期望的值。 所以最后用了一個orElse的方法,當Optional為空的時候返回一個默認值。
獲取Optional對象的值:
1. get() 是這些方法中最簡單但最不安全的方法。如果變量存在,直接返回封裝的變量值。否則拋出一個NoSuchElementException異常。
2. orElse(T other) 默認值,當值存在返回值,否則返回此默認值。
3. orElseGet(Supplier<? extends T> other) 是orElse方法的延遲調用版,Supplier方法只有在Optional對象不含值時才執行調用。
4. orElseThrow(Supplier<? extends X> exceptionSupplier )和get方法相似,遇到Optional對象為空時都拋出一個異常,使用orElseThrow可以自定義異常類型。
5. ifPresent(Consumer<? super T>) 在變量值存在時執行,否則什么都不做。
判斷Optional是否有值 isPresent()
假設有一個方法,接受兩個參數 Person 和Car 來查詢便宜的保險公司:
public Insurance getInsurance(Person person ,Car car){ //業務邏輯 return new Insurance(); }
這是以前的版本,使用我們今天所學的知識 可以做一個安全版本,它接受兩個Optional對象作為參數 返回值也是一個Optional<Insurance>方法:
public static Optional<Insurance> getInsuranceOpt(Optional<Person> person,Optional<Car> car){ if(person.isPresent() && car.isPresent()){ return Optional.of(getInsurance(person.get(),car.get())); } return Optional.empty(); }
這看起來好了很多,更優雅的方式:
public static Optional<Insurance> getInsuranceOpt1(Optional<Person> person, Optional<Car> car) { return person.flatMap(p -> car.map(c -> getInsurance(p, c))); }
如果p為空,不會執行返回空的Optional對象。如果car為空也不會執行 返回空Optional對象。 如果都有值那么調用這個方法。
filter剔除特定的值
除了map和flatMap方法類似流中的操作,還有filter方法。使用filter可以快速判斷Optional對象中是否包含指定的規則,如:
Insurance insurance = new Insurance(); if(insurance != null && insurance.getName().equals("abc")){ System.out.println("is abc"); }
可以使用filter改寫為:
Optional<Insurance> insuranceOpt = Optional.of(insurance);
insuranceOpt.filter(c->c.getName().equals("abc")).ifPresent(x->System.out.println(x));
用Optional改善你的代碼
我們雖然很難對老的Java API進行改動,但是可以再自己的代碼中添加一些工具方法,來修復或者繞過這些問題,容納給你的代碼享有Optional帶來的威力。
使用Optional封裝可能為null的值
現存的Java API幾乎都是通過返回一個null的方式來表示需要值的缺失,或者由於某些原因計算無法得到該值。比如,如果Map中不含指定的鍵對應的值,它的get就會返回一個null。我們想在這種情況下返回Optional對象是很容易的。
Object value = new HashMap<String,Object>().get("key"); //null
有兩種方式轉換為Optional對象,第一種就是if else 方式,顯然很笨重。第二種就是使用ofNullable方法。
Optional<Object> value = Optional.ofNullable(new HashMap<String,Object>().get("key"));
每次你希望安全的對潛在為null的對象進行轉換時,都可以先將其轉換為Optional對象。
異常與Optional
由於某種原因,函數無法返回某個值,這時除了返回null,還會拋出一個異常。典型的例子是Integer.parseInt(String),將String轉換為int。如果String無法解析為整型,就會拋出NumberFormatException異常。一般做這個操作,我們會加入 try/catch來避免程序掛掉,而不是用if來判斷。
使用Optional對象對遭遇無法轉換的String返回非法值進行建模,這時你期望parseInt的返回值是一個optional。雖然我們無法改變以前的方法,但我們可以創建一個工具方法:
public static Optional<Integer> StringToInt(String s){ try{ return Optional.of(Integer.parseInt(s)); }catch (Exception ex){ return Optional.empty(); } }
我們可以建立一個OptionalUtils工具類,然后對所有的類似轉換操作創建方法。然后在需要的地方 OptionalUtils.StringToInt(Stirng);
基礎類型的Optional對象
與Stream對象一樣,Optional對象也提供了類似的基礎類型:OptionalInt、OptionalDouble、OptionalLong。 但是這三個基礎類型不支持map、flatMap、filter方法。
小結:
1.null引用在歷史上被引入到程序設計語言中,目的是為了表示變量值的缺失。
2.Java 8中加入了一個新的類 java.util.Optional<T> 對存在或缺失的變量進行建模。
3.你可以使用靜態工廠方法Optional.empty、Optional.of、Optional.ofNullable創建Optional對象。
4.Optional支持多種方法,比如map、flatMap、filter,他們在概念上與Stream類似。
5.使用Optional會迫使你更積極的引用Optional對象,以及應對變量缺失的問題,最終你能更有效的防治代碼中出現空指針異常。
6.使用Optional能幫助你更好的設計API,用戶只需要參閱簽名酒知道該方法是否接受一個Optional。