Java 8 開始新增的 Optional 類 - Optional 對象中的返回

語言: CN / TW / HK

使用 get() 來返回一個值

在對 Optional 對象完成一些檢查和校驗後,我們可以使用

get()

方法來返回對象中的值。

    // returning Value With get()
    @Test
    public void givenOptional_whenGetsValue_thenCorrect() {
        Optional<String> opt = Optional.of("HoneyMoose");
        String name = opt.get();
        assertEquals("HoneyMoose", name);
    }

orElse() 或者 orElseGet() 方法不一樣的地方是 get() 只會在 Optional 包裝的對象不為 null 的時候返回值,否則這個方法將會拋出一個沒有這個元素(no such element exception)的異常 。

    @Test(expected = NoSuchElementException.class)
    public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
        Optional<String> opt = Optional.ofNullable(null);
        String name = opt.get();
    }

上面的方法顯示瞭如何使用

get()

方法來獲得 Optional 中元素的典型操作。

我們使用 Optional 的主要原因就是為了避免在程序中出現 Null 對象異常的這種情況,但是 get() 方法的這種操作還是會給你帶來空對象異常的。

因此需要注意下這種代碼編寫方式,也有可能在 JDK 的後續版本中,這個 get() 方法有可能被取消掉,但是目前還不會。

optional-failure

正是因為這種情況,get() 這個方法也有可能出現空對象異常,在編碼的時候還是需要注意下的。

使用 filter() 來進行條件返回

我們可以使用 filter() 方法在輸出之前進行測試,然後過濾出滿足我們條件的返回對象。

這個方法將會使用 Java 提供的謂語(predicate )作為參數來返回 Optional 對象。

如果通過了 Java 提供的謂語(predicate )測試的話,Optional 對象將會被原樣返回。

如果,測試的 謂語(predicate )為 False 的話,那麼一個空的 Optional 對象將會被返回。

    @Test
    public void whenOptionalFilterWorks_thenCorrect() {
        Integer year = 2016;
        Optional<Integer> yearOptional = Optional.of(year);
        boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
        assertTrue(is2016);
        boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
        assertFalse(is2017);
    }

filter() 方法通常用來處理你不需要的返回,或者處理滿足你條件的返回。

例如在對用户電子郵件進行檢查,或者用户密碼進行檢查的時候,我們可以設置這樣一個 filter() 過濾器,當不滿足我們設置條件的時候,我們讓程序返回一個空的對象,以此來設置條件。

讓我們看另外一個使用場景,我們希望購買一個調制解調器(modem),但是我們只關注的是價格,我們對信號燈並不敏感

我們希望對調制解調器在滿足價格區間的時候獲得一個通知:

public class Modem {
    private Double price;

    public Modem(Double price) {
        this.price = price;
    }
    // standard getters and setters
}

讓我們讓這個對象通過一些代碼來檢查這個對象是不是滿足我們設定的價格區間.

下面的代碼是我們不使用 Optional 的時候的代碼。

從上面的代碼來看,我們需要進行 Null 檢查,然後獲得價格,然後判斷價格,更要命的更極端的情況價格也有可能為 null。

public boolean priceIsInRange1(Modem modem) {
    boolean isInRange = false;

    if (modem != null && modem.getPrice() != null 
      && (modem.getPrice() >= 10 
        && modem.getPrice() <= 15)) {

        isInRange = true;
    }
    return isInRange;
}

為了滿足 Null 檢查的所有條件,我們需要不停的 if 進行判斷。我們需要通過這些 if 條件檢查來確定是否滿足我們的條件,並且這個代碼看起來有點鬱悶,但是實際上也確實就是這樣寫的。

@Test
public void whenFiltersWithoutOptional_thenCorrect() {
    assertTrue(priceIsInRange1(new Modem(10.0)));
    assertFalse(priceIsInRange1(new Modem(9.9)));
    assertFalse(priceIsInRange1(new Modem(null)));
    assertFalse(priceIsInRange1(new Modem(15.5)));
    assertFalse(priceIsInRange1(null));
}

同時,你也有可能忘記某一個檢查,比如説價格為 NULL 的時候怎麼辦。

這個檢查在編譯的時候是不會提示你的,只有程序真正上線運行了,出現了異常了,你才知道,我又忘記檢查空了。

現在我們看看 Optional 中的 filter() 是怎麼做的。

    public boolean priceIsInRange2(Modem modem2) {
        return Optional.ofNullable(modem2).map(Modem::getPrice).filter(p -> p >= 10).filter(p -> p <= 15).isPresent();
    }

你的程序精簡到只需要一行就可以了。

map 這個方法只是簡單的從對象中獲得值,後面的過濾器才是對獲得值進過濾的。

需要注意的是,使用 filter() 不會對輸入的參數進行修改。

在我們的用例中,我們非常容易的就從我們的 Model 對象中獲得了價格的屬性。至於 map() 的使用我們在後面的內容中進行介紹。

針對上面的代碼,首先如果對象出現 null 的時候是不會對你程序有任何影響的,還是能一直跑下去的。

其次就是邏輯非常簡單,整個邏輯就是對價格進行判斷,至於其他的 null 判斷都是由 Optional 完成的。

@Test
public void whenFiltersWithOptional_thenCorrect() {
    assertTrue(priceIsInRange2(new Modem(10.0)));
    assertFalse(priceIsInRange2(new Modem(9.9)));
    assertFalse(priceIsInRange2(new Modem(null)));
    assertFalse(priceIsInRange2(new Modem(15.5)));
    assertFalse(priceIsInRange2(null));
}

最開始的 If 代碼也是可以完成價格的判斷的,但是這個方法有着自身的缺陷,因此我們使用了 Optional 提供的 filter() 來進行了改進,並且使用了 filter() 來替代了 IF 代碼判斷來過濾掉我們不需要的值。

使用 map() 來轉換值

在上面的內容中,我們介紹瞭如何使用 filter() 來過濾掉我們不需要的值,換句話説就是有條件的拒絕和通過。

使用 map() 的句法和使用 filter() 的句法是差不多的。

    @Test
    public void givenOptional_whenMapWorks_thenCorrect() {
        List<String> companyNames = Arrays.asList("paypal", "oracle", "", "microsoft", "", "apple");
        Optional<List<String>> listOptional = Optional.of(companyNames);

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

在這個例子中,我們使用了一個包含有 Optional 對象的 List 來測試 map() 的使用。

這個例子中,我們使用了 map() 返回了 List 的長度。

map() 方法將會返回對 Optional 內部包裝的計算,我們需要調用正確的函數才能夠返回正確的值。

需要注意的是 filter() 只是檢查對象中的值是不是滿足給定的條件,map() 需要做的操作就更近一步了, map() 需要獲得 Optional 對象中的值,然後進行計算,在完成計算後將計算的結果進行返回。

    @Test
    public void givenOptional_whenMapWorks_thenCorrect2() {
        String name = "HoneyMoose";
        Optional<String> nameOptional = Optional.of(name);

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

我們還可以將

map

filter

結合在一起使用,這種結合能夠為你的代碼提供更加強勁的計算能力。

在 Java 8 介紹的 Stream 中,我們也通常是這樣一起結合使用的,

考察下面的使用場景,我們需要對用户的密碼進行檢查是否滿足條件,在這個檢查之前,我們首先需要對用户輸入的密碼進行清理,比如説去除掉前後的空白等,我們可以使用 map() 方法首先對密碼進行清理,然後使用 filter() 方法對清理後的密碼進行判斷是否滿足條件。

    @Test
    public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
        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);
    }

上面的代碼就比較明確的展示了這個過程,首先進行清理,然後進行過濾。

使用 flatMap() 來轉換值

map() 相同,我們同時還有一個 flatMap() 方法作為可選的方法來進行使用。

2 者不同的地方就是 map() 只能對值進行轉換,flatMap() 可以對包裝的對象進行計算。

簡單來説就是 flatMap() 將包裝後的對象,進行解開包裝,然後進行計算。

如果上面我們説到的例子,我們對簡單的

String

Integer

對象包裝成了 Optional 實例,但是實際上我們使用 Optional 包裝的對象比這個要複雜得多得多。

下面我們就對這個區別進行一些説明,假設我們有一個 Person 對象,在這個 Person 對象中我們有 name,age 和 password 3 個屬性。

我們定義的類如下:

public class Person {
    private String name;
    private int age;
    private String 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
}

我們可以將上面的類實例化成對象後,使用 Optional 來進行包裝,這種包裝的過程就和我們包裝 String 是一樣的。

例如,我們可以使用下面的方法來完成對這個對象的 Optional 的包裝:

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

注意,我們包裝了

Person

這個對象,這個對象是 Optional 的一個示例。

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

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

        String name = personOptional.flatMap(Person::getName).orElseThrow(IllegalArgumentException::new);
        assertEquals("john", name);
    }

現在,我們嘗試從我們使用 Optional 包裝好的 Person 對象中獲得用户名字的屬性。

注意上面使用的代碼,使用 map() 代碼也是可以完成的,但是你需要分開 2 次才能獲得你需要的值。

Person::getName

這個方法和

String::trim

這個方法的調用是類似的,只是一個是調用 JDK 提供的方法罷了。

考慮這樣一個問題,假設我們的對象中有對象,對象中再有對象,還有對象中有 List ,Map 這樣比較複雜的數據類型我們應該怎麼呢。

我們是不是要不停的解包,解包再解包,這太難了。

這個時候使用 flatMap() 就可以一句話搞定了。我們對對象中屬性可能使用 Optional 完成了解包。這樣代碼的可讀性就更高了。

https://www.ossez.com/t/java-8-optional-optional/13969