使用Optional,不再頭疼NPE

後青春期的Keats發表於2020-05-13

前言

在 Java 語言開發中,可能大多數程式設計師遇到最多的異常就是 NullPointException 空指標異常了。這個當初語言的開發者“僅僅因為這樣實現起來更容易”而允許空引用所帶來的代價是非常慘痛的。而我們開發者不得不使用多重 if 巢狀判斷來規避 NPE 或者通過多個 if 結合 return 語句來終止程式。且看一個例子

假如需要處理下面的巢狀物件,這是一個用於汽車、汽車保險的客戶。

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 getCarInsuranceNames(Person person) {
    return person.getCar().getInsurance().getName();
}

沒錯,當這個人沒有車 / 他的車沒有上保險時,程式碼會丟擲 NPE。或者說這個人根本就是 null,也會直接丟擲異常。我們常見的作法就是在每次 get 方法之後,進行 if 判斷,增加程式碼的健壯性。可是這樣程式碼會顯得十分臃腫。Java 語言的開發者們也在關注著這些問題。因此在 Java8 提供了新的 API:java.util.Optional 用來優雅的處理 null。接下來就請讀者和我一起揭開 Optional 神祕的面紗吧!

PS:Optional 類提供的很多 API 結合 Lambda 表示式食用更佳,另外還有很多 API 和 Stream 流中同名 API 的思想基本一致。因此建議讀者先行了解這兩個知識點,可以在我的部落格 Java8新特性 標籤下學習

宣告:本文首發於部落格園,作者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!

Optional 入門

Optional 類是一個封裝 T 值的類,變數 T 存在時,Optional 只是對他做一個簡單的封裝,如果 T 不存在,缺失的值會被建模成一個空物件,由 Optional.empty() 返回。下面這張圖可以形象的描述 Optional

1589295110199

現在我們嘗試著重構之前關於 人 車 保險 的程式碼

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;
    }
}
public class Insurance {
    private String name;
    public String getName() {
        return name;
    }
}

注意:對於保險來說,我們從邏輯層面限定每個保險公司都有名稱,如果沒有,那一般是資料出了問題而非程式碼的問題,開發者應該著手去尋找為什麼資料庫存在名字為空的保險公司。而不是這裡丟擲 NPE,故而我們不用將 Insurance 的 name 欄位使用 Optional 包裹

通過上面的程式碼,我們已經將物件由 Optional 所包裹了,那接下來我們該如何使用它呢?

建立 Optional 物件

建立一個空物件

Optional<Object> empty = Optional.empty();

Optional.empty(); 該方法返回一個空物件,

根據一個非空值建立

Optional<Car> car = Optional.of(c);

Optional.of(T t); 方法會返回一個 Optional 物件,但是需要注意,如果 of 方法引數是 null,該行會丟擲 NPE。

允許空值建立

Optional car = Optional.ofNullable(c);

為了避免在建立 Optional 物件時,由於源物件為空而引發的 NPE,該類還提供了 ofNullable 方法,當引數為 null 時,返回 Optional.empty()。內部的 API 是這樣的

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

map --- 從 Optional 中提取和轉換值

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

Optional 類提供 map 方法,接收一個函式式介面 Function 的實現類,如果呼叫者是空的,則返回 empty(),否則對 Optional 中的物件 value 呼叫 Function 實現類中的 apply() 方法,再包裝成 Optional 返回。可以用下面的圖直觀的看到 map 執行的過程:

1589296375952

請注意,在 map 執行完 apply 方法拿到返回值之後,會主動將返回值再次包裹成 Optional 物件。因此我們如果按照下面的方式改造我們之前的方法,編譯是無法通過的:

person.map(Person::getCar).map(Car::getInsurance).map(Insurance::getName);

我們來分析一下: person.map(Person::getCar) 改造後的 person 類中, getCar 方法返回 Optional 物件,而 map 又將 Optional 包裝到 Optional 中,形成 Optional<Optional> emmm...套娃式包裝。兩層包裝的 car 是無法通過 map 在呼叫 getInsurance 方法的。

幸運的是,和 Stream 一樣,Optional 也提供了扁平化流的方法 flatMap()。且看原始碼

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        @SuppressWarnings("unchecked")
        Optional<U> r = (Optional<U>) mapper.apply(value);
        return Objects.requireNonNull(r);
    }
}

flatMap() 比 map() 方法多了一個執行完後將巢狀 Optional 強轉成 Optional 的操作,避免了流不能繼續使用的尷尬處境。因此,我們可以將獲取保險公司名稱的方法改造成下面這樣:

public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
        .flatMap(Car::getInsurance)
        .map(Insurance::getName)
        .orElse("Unknown");
}

其中 orElse() 方法表示當最終 Optional 包裹的物件還是空時,返回的預設值

PS:由於 Optional 並沒有實現序列化介面,因此如果你的專案中使用了某些要求序列化的框架,並且在某個類中使用 Optional 包裹了欄位。可能會由序列化引發程式故障。

操作 Optional 中的變數

get()

通過 get() 方法獲取變數,如果變數存在就直接得到該變數,否則丟擲一個 throw new NoSuchElementException("No value present"); 異常。一般不建議使用該方法畢竟直接用 get() 方法了,還要整 Optional 這些花裡胡哨的幹啥呢

orElse()

在物件為 null 時提供一個預設值

orElseGet(Supplier<? extends T> other)

在物件為 null 通過呼叫 supplier 提供者介面的實現,返回一個值

orElseThrow()

在物件為 null 丟擲一個可定製的異常資訊,可以用來丟擲專案中的自定義異常,以便全域性異常捕獲器抓取及響應資料

ifPresent(Consumer<? super T> action)

當物件不為 null 時,執行消費者操作。為 null 時啥也不幹

更優雅的判斷語句

我們常常呼叫某個物件的某個方法去判斷其屬性。為了安全操作。首先需要對該物件進行非空校驗。例如要檢查保險公司名稱是否為 Keats,需要這麼寫

if(i != null && "Keats".equals(i.getName())){
    System.out.println("yes");
}

現在我們可以這麼寫

Optional<Insurance> insurance = Optional.ofNullable(i);
insurance.filter(in -> "Keats".equals(in.getName())).ifPresent(in -> System.out.println("yes"));

先看 filter 的原始碼

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent()) {
        return this;
    } else {
        return predicate.test(value) ? this : empty();
    }
}

首先第一步檢查了謂詞實現非空,第二步判斷 Optional 中的物件如果為空則返回空 Optional,如果不為空執行謂詞方法,條件成立則返回該物件。否則返回空 Optional。即僅當 Optional 中物件不為 null 且符合條件時,返回該物件之後通過 ifPresent() 方法執行接下來的邏輯。非常方便易懂

其他

Optional 還提供了一些基礎型別物件對應的類,如 OptionalInt、OptionalLong 同 Stream 流一樣,採用基本操作型別處理資料,避免了自動拆裝箱帶來的效能損失。但卻犧牲了 map、flatMap、filter 方法。開發中需酌情使用

碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看到吧!

相關文章