Java 8 開始新增的 Optional 類 - Optional 物件中的返回
使用 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()
方法有可能被取消掉,但是目前還不會。
正是因為這種情況,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 完成了解包。這樣程式碼的可讀性就更高了。