1. 程式人生 > >Java 8新特性之Optional取代null

Java 8新特性之Optional取代null

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。