Java 8 Optional入門實(shí)戰(zhàn)

1. 簡介

本文簡要介紹一下Java 8 引入的 Optional 類。引入Optional 類的主要目的是為使用可選值代替 null 提供類型級解決方案。如果,你想知道為什么需要更深入的了解和使用 Optional 類,可以參考甲骨文官方文章。

Optionaljava.util.package 的一部分,為了能夠使用,需要導(dǎo)入Optional

    import java.util.Optional;

2. 創(chuàng)建 Optional 對象

有多種方式可以創(chuàng)建 Optional 對象,可以使用下面的方法創(chuàng)建一個空的 Optianal對象:

    @Test
    public void test_createsEmptyOptionalObject() throws Exception {
        Optional<String> empty = Optional.empty();
        assertFalse(empty.isPresent());
    }

可以使用 isPresent API 來檢查 Optional 對象是否有封裝的值,當(dāng)且僅當(dāng) * Optional* 封裝了非 null 值時,API才返回 true。

還可以使用 Optional 提供了靜態(tài)方法創(chuàng)建 Optional 對象:

    @Test
    public void test_createOptionalObjectWithStaticMethod() throws Exception {
        String val = "not null";
        Optional<String> hasVal = Optional.of(val);
        assertTrue(hasVal.isPresent());
    }

如果 Optional 對象有封裝的值(非 null ),可以對封裝的值進(jìn)行處理:

    @Test
    public void test_processOptionalValue() throws Exception {
        String val = "not null";
        Optional<String> hasVal = Optional.of(val);
        System.out.println(hasVal.toString());
        assertEquals("Optional[not null]", hasVal.toString());
    }

當(dāng)使用 Optional 提供的靜態(tài)方法 of 創(chuàng)建 Optional 對象時,方法的參數(shù)不能null,否則,方法會拋出 NullPointerException

    @Test(expected = NullPointerException.class)
    public void test_throwNullPointerException() throws Exception {
        String val = null;
        Optional<String> hasVal = Optional.of(val);
    }

如果構(gòu)建 Optional 對象時可以傳入 null 參數(shù),可以使用 ofNullable 方法代替of

    @Test
    public void test_passNullParamNoException() throws Exception {
        String val = null;
        Optional<String> hasVal = Optional.ofNullable(val);
        assertFalse(hasVal.isPresent());
    }

使用 ofNullable 方法創(chuàng)建 Optional 對象時,如果傳入一個 null 參數(shù),方法不會拋出異常,而是返回一個空的 Optional 對象,和使用 Optional.empty API 創(chuàng)建的一樣。

3. 檢查值是否存在

當(dāng)?shù)玫揭粋€從其他方法返回或自己創(chuàng)建的 Optional 對象后,可以使用isPresent API 檢查 Optional 對象是否有封裝值:

    @Test
    public void test_checkValuePresentOrNot() throws Exception {
        Optional<String> opt = Optional.of("has value");
        assertTrue(opt.isPresent());

        opt = Optional.ofNullable(null);
        assertFalse(opt.isPresent());
    }

當(dāng)且僅當(dāng)Optional 對象封裝一個非空值時,isPresent API才返回true。

在Java 11 中可以使用 isEmpty API 完成相反的工作:

    @Test
    public void test_checkValuePresentOrNotJava11() throws Exception {
        Optional<String> opt = Optional.of("has value");
        assertFalse(opt.isEmpty());

        opt = Optional.ofNullable(null);
        assertTrue(opt.isEmpty());
    }

當(dāng)且僅當(dāng) Optional 對象封裝的值為 null 時,isEmpty 返回true,其他情況返回false。

4. 使用 ifPresent() 進(jìn)行條件處理

ifPresent API 允許我們在 Optional 對象封裝的值非空時執(zhí)行一些代碼,在沒有Optional 之前,最常用的方法是使用 if 語句進(jìn)行判斷,結(jié)果為真時執(zhí)行代碼邏輯:

    if(name != null){
        System.out.println(name.length);
    }

這段代碼在執(zhí)行其他代碼之前先檢查 name 變量是否為 null。冗長并不是這種方法的唯一問題一,這種方法固有很多潛在的bug。

在習(xí)慣了這種方法之后,很容易忘記在代碼的某些部分執(zhí)行空檢查。如果 null 值進(jìn)入該代碼,可能會在運(yùn)行時導(dǎo)致 NullPointerException 異常。 當(dāng)程序因輸入問題而失敗時,通常是編碼不夠健壯導(dǎo)致,也是代碼實(shí)踐不好的結(jié)果。

作為強(qiáng)制執(zhí)行良好編程實(shí)踐的一種方式,Optional 可以明確地處理 null。 在典型的函數(shù)式編程風(fēng)格中,我們可以對實(shí)際存在的對象執(zhí)行操作,使用Java 8重構(gòu)上面的代碼如下:

    @Test
    public void doSomeThingWhenExist()  throws Exception {
        Optional<String> opt = Optional.of("baeldung");
        opt.ifPresent(name -> System.out.println(name.length()));
    }

5. 使用 orElse 獲取封裝的值

orElse API 用于從 Optional 實(shí)例中獲取封裝的值,orElse 的唯一參數(shù)作為Optional 無封裝值時的默認(rèn)值,這點(diǎn)類似 System.getProperty API。如果,Optional 有封裝值 orElse API返回 Optional 封裝的值,否則返回參數(shù)的值。

    @Test
    public void test_getValueUseorElse() throws Exception {
        Optional<String> hasVal = Optional.of("Hello");
        String val = hasVal.orElse("no value");
        assertEquals("Hello", val);

        Optional<String> noVal = Optional.empty();
        String defaultVal = noVal.orElse("default");
        assertEquals("default", defaultVal);
    }

6. 使用 orElseGet 獲封裝的值

orElseGet API 功能和 orElse 類似,兩者的不同之處在于 orElseGet 的參數(shù)為一個 Supplier 實(shí)例,當(dāng) Optional 對象無封裝值時,orElseGet 調(diào)用 Supplier 實(shí)例的 get 方法,并將返回值作為 orElseGet 的返回值。

    @Test
    public void test_getValueUseorElseget() throws Exception {
        Optional<String> hasVal = Optional.of("Hello");
        String val = hasVal.orElseGet(() -> "no value");
        assertEquals("Hello", val);

        Optional<String> noVal = Optional.empty();
        String defaultVal = noVal.orElseGet(() -> "default");
        assertEquals("default", defaultVal);
    }

7. orElseorElseGet 的區(qū)別

Optional 對象無封裝值時,orElseorElseGet 并無本質(zhì)上的區(qū)別,兩個API 都返回各自的默認(rèn)值。但是,當(dāng) Optional 對象有封裝值時兩者有很大的區(qū)別,而且兩者在性能上的差異也十分明顯。一句話總結(jié)兩者的差異就是:orElse 會觸發(fā)獲取默認(rèn)值的動作,盡管并不需要。為了更加形象的說明,這里提供一個方法用于獲取默認(rèn)值,方法中使用 sleep 模擬這是一個耗時的操作:

    private String getDefaultValue() {
        System.out.println("enter method get default value");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "default value";
    }

創(chuàng)建一個非空的 Optional 對象,分別調(diào)用 orElseorElseGet 方法,觀察兩者行為上的差異:

    @Test
    public void test_differenceorElseAndorElseGet() throws Exception {
        Optional<String> hasVal = Optional.of("value");
        System.out.println("enter orElse method");
        String var0 = hasVal.orElse(getDefaultValue());

        System.out.println("enter orElseGet method");
        String var1 = hasVal.orElseGet(this::getDefaultValue);
    }

上面代碼的輸出結(jié)果如下:

enter orElse method
enter method get default value
enter orElseGet method

從輸出結(jié)果可以非常清晰的看出兩個API之間的差異,為了更好的性能,在編碼中優(yōu)先使用 orElseGet API 獲取 Optional 的值。。

8. 使用 orElseThrow 拋出異常

orElseThroworElseorElseGet API類似,orElseThrow 提供了一種在Optional 為空時的處理方法-拋異常而不是返回默認(rèn)值。

    @Test(expected = IllegalArgumentException.class)
    public void test_throwsExecption() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).orElseThrow(
                IllegalArgumentException::new);
    }

9. 使用 get() 獲取值

get 是獲取 * Optional* 值的最后方法(不是一個好方法):

    @Test
    public void test_getValueUseGet() {
        Optional<String> opt = Optional.of("value");
        String name = opt.get();
        assertEquals("value", name);
    }

和上面三種獲取值的方法不同,* get * 方法只能返回 Optional 封裝的值,如果Optional 為空,方法會拋出 NoSuchElementException 異常。

    @Test(expected = NoSuchElementException.class)
    public void test_throwsNoSuchElementException() {
        String nullName = null;
        String name = Optional.ofNullable(nullName).get();
    }

拋出異常是 get API 的最大缺陷,Optional 應(yīng)該幫助我們盡可能屏蔽這些不可見異常,因此 get API 和 * Optional* 目標(biāo)相背而馳,該方法將來可能被廢棄。應(yīng)該盡可能的使用其他方法獲取值。

10. 使用 filter() 進(jìn)行過濾

filter API 被用于對 Optional 封裝的值進(jìn)行一個內(nèi)聯(lián)測試,filter API 使用一個謂詞作為參數(shù)并返回一個Optional 對象。如果,被封裝的值通過測試則返回Optional 本身,否則返回一個空的 Optional 對象。

    @Test
    public void test_filter() throws Exception {
        Optional<Integer> passTest = Optional.of(101);
        assertTrue(passTest.filter(integer -> integer.intValue() > 100).isPresent());
        Optional<Integer> notPassTest = Optional.of(99);
        assertFalse(notPassTest.filter(integer -> integer.intValue() > 100).isPresent());
    }

filter API 的工作套路:根據(jù)某個預(yù)定義的規(guī)則拒絕 Optional 對象封裝的值,可以用于拒絕格式錯誤的郵箱地址或強(qiáng)度不夠的密碼。

接下來看一個更有趣的例子(有些場景下不使用 Optional 為了安全的操作,我們通常需要進(jìn)行多次 null 檢查)。假設(shè),我們打算購買一部手機(jī)并且只關(guān)心手機(jī)的價格。我們從手機(jī)購買網(wǎng)站得到手機(jī)價格的推送消息,手機(jī)價格被封裝在一個對象中,數(shù)據(jù)結(jié)構(gòu)定義如下:

public class Phone {
    private Double price;

    public Phone(Double price) {
        this.price = price;
    }

    //standard getters and setters
}

當(dāng)把網(wǎng)址的推送數(shù)據(jù)傳遞給檢查手機(jī)價格是否滿足我們的預(yù)算要求的函數(shù)時(假設(shè)能接受的手機(jī)價格為3000-5000),如果不使用 * Optional* 一種可能的代碼實(shí)現(xiàn)如下:

    public boolean checkPriceWithoutOptional(Phone phone) {
        boolean isInRange = false;

        if (phone != null && phone.getPrice() != null
                && (phone.getPrice() >= 3000
                && phone.getPrice() <= 5000)) {

            isInRange = true;
        }
        return isInRange;
    }

為了實(shí)現(xiàn)上面的功能我們寫了很多代碼,尤其在 if 的條件表達(dá)式中,函數(shù)真正的核心代碼僅僅是檢查價格范圍,其他多余的檢查對于實(shí)現(xiàn)功能來說都是不必要的。代碼冗余可能并不是最嚴(yán)重的問題,忘記 null 檢查可能更加糟糕,而這不會引發(fā)任何編譯錯誤(代碼靜態(tài)檢查工具可以發(fā)現(xiàn)并上報告警)。

使用 Optionalfilter API 可以以一種優(yōu)雅的方式實(shí)現(xiàn)同樣的功能:

    public boolean checkPriceWithOptional(Phone phone) {
        return Optional.ofNullable(phone)
                .map(Phone::getPrice)
                .filter(p -> p >= 3000)
                .filter(p -> p <= 5000)
                .isPresent();
    }

使用 Optional 讓代碼在以下兩點(diǎn)優(yōu)于使用 if 語句檢查:

  • 給函數(shù)出入一個 null 對象,不會觸發(fā)任何錯誤。
  • 代碼更加聚焦業(yè)務(wù)實(shí)現(xiàn)(價格檢查),其他的事情由 Optional 負(fù)責(zé)。

11. 使用 map() 進(jìn)行值變換

在之前的章節(jié),我們已經(jīng)看到如何使用過濾器接受或拒絕 Optional 封裝的值。相同的語法可以用于 map API 對 Optional 封裝的值進(jìn)行變換。

    @Test
    public void test_mapList2ListSize() {
        List<String> companyNames = Arrays.asList(
                "Java", "C++", "", "C", "", "Python");
        Optional<List<String>> listOptional = Optional.of(companyNames);

        int size = listOptional
                .map(List::size)
                .orElse(0);
        assertEquals(6, size);
    }

在上面的例子中,我們使用 Optional 封裝了一個字符串列表,并使用 map API 對 字符串列表進(jìn)行變換,上面例子中執(zhí)行的變化是獲取字符串列表的長度。

map API 返回對 Optional 封裝對象的計(jì)算結(jié)果,最后需要調(diào)用合適的API來獲取Optional 對象的值(變換后的值)。

注意:filter API 值檢查 Optional 對象封裝的值并返回一個boolean類型的結(jié)果,相反 map API 對 Optional 對象封裝的值進(jìn)行計(jì)算并返回計(jì)算結(jié)果。

    @Test
    public void test_mapString2StringSize() {
        String name = "Hello World";
        Optional<String> nameOptional = Optional.of(name);

        int len = nameOptional
                .map(String::length)
                .orElse(0);
        assertEquals(11, len);
    }

我們可以鏈?zhǔn)秸{(diào)用 mapfilter API 來做一些更有意義的事情。假設(shè),我們有一段代碼需要檢查用戶輸入的密碼是否正確,我們可以使用 map 對密碼進(jìn)行變換,使用 filter 判斷密碼是否正確:

    @Test
    public void test_checkPassword() {
        String password = " password ";
        Optional<String> passOpt = Optional.of(password);
        boolean correctPassword = passOpt.filter(
            pass -> pass.equals("password")).isPresent();
        assertFalse(correctPassword);

        correctPassword = passOpt
            .map(String::trim)
            .filter(pass -> pass.equals("password"))
            .isPresent();
        assertTrue(correctPassword);
    }
}

12. 使用 flatMap() 對值進(jìn)行變換

map API 一樣,我們也可以使用 flatMap API 作為一個替代方法對值進(jìn)行變換。兩者的主要區(qū)別是:map 值對未封裝的值進(jìn)行轉(zhuǎn)換,flatMap 在處理值之前先進(jìn)行“去封裝”操作,然后再執(zhí)行變換操作。
為了更清晰的解釋兩者的區(qū)別,我們假設(shè)有一個Person對象,對象有三個基本屬性:名字、年齡和密碼。

public class Person {
    private String name;
    private int age;
    private String password;

    public Person() {
    }

    public Person(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }

    public Optional<Integer> getAge() {
        return Optional.ofNullable(age);
    }

    public Optional<String> getPassword() {
        return Optional.ofNullable(password);
    }

    // normal constructors and setters
}

我們創(chuàng)建一個Person對象,并使用 Optional 封裝創(chuàng)建的Person對象:

        Person person = new Person("john", 26, "pwd");
        Optional<Person> personOptional = Optional.of(person);

分別使用 mapflatMap API 獲取名字的代碼如下,從中可以看到使用 flatMap API 的代碼量較使用 map 更短小,也更加容易理解。

    @Test
    public void test_flatMap() {
        Person person = new Person("ct", 26,"pwd");
        Optional<Person> personOptional = Optional.of(person);

        Optional<Optional<String>> nameOptionalWrapper
            = personOptional.map(Person::getName);
        Optional<String> nameOptional
            = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
        String name1 = nameOptional.orElse("");
        assertEquals("ct", name1);

        String name = personOptional
            .flatMap(Person::getName)
            .orElse("");
        assertEquals("ct", name);
    }

13. 總結(jié)

本文簡要介紹了Java 8 Optional 類的大部分重要特性,與此同時,我們也簡單闡述了為什么我們選擇使用Optional 代替顯示的 null 檢查和參數(shù)檢查。最后,講解了 orElseorElseGet 之間微妙但重要的區(qū)別,關(guān)于該主題可以從拓展閱讀獲取更多內(nèi)容。

文中的樣例代碼可以從 GitHub.獲取。

參考

[1] Guide To Java 8 Optional
[2] Java 8 Optional
[3] Java 11 Optional

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Optional 本章內(nèi)容 如何為缺失的值建模 Optional 類 應(yīng)用Optional的幾種模式 使用Opti...
    追憶逝水年華閱讀 1,814評論 0 0
  • 本文獲得Stackify授權(quán)翻譯發(fā)表,轉(zhuǎn)載需要注明來自公眾號EAWorld。 作者:EUGEN PARASCHIV...
    72a1f772fe47閱讀 11,787評論 3 7
  • 這篇文章寫得不錯,所以轉(zhuǎn)載了下,修改了小部分,原文地址見末尾 身為一名Java程序員,大家可能都有這樣的經(jīng)歷:調(diào)用...
    瘋狂的冰塊閱讀 277評論 0 2
  • 文/黃煊墨 我小時候睡懶覺、干活偷懶,我媽必“詛咒”我,長大娶不到媳婦。當(dāng)時太小,不知道媳婦是什么東西,但老太太總...
    煊墨雜談閱讀 661評論 0 0
  • 人生的路有平坦也有坎坷 平坦坎坷都得走 人生的河有深也有淺 深淺都得趟 人生的風(fēng)景有美麗也有蕭條 美麗蕭條都得欣賞...
    文采樂閱讀 340評論 8 14