1. 簡介
本文簡要介紹一下Java 8 引入的 Optional 類。引入Optional 類的主要目的是為使用可選值代替 null 提供類型級解決方案。如果,你想知道為什么需要更深入的了解和使用 Optional 類,可以參考甲骨文官方文章。
Optional 是 java.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. orElse 和 orElseGet 的區(qū)別
在 Optional 對象無封裝值時,orElse 和 orElseGet 并無本質(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)用 orElse 和 orElseGet 方法,觀察兩者行為上的差異:
@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 拋出異常
orElseThrow 與 orElse 和 orElseGet 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)并上報告警)。
使用 Optional 的 filter 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)用 map 和 filter 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);
分別使用 map 和 flatMap 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ù)檢查。最后,講解了 orElse 和 orElseGet 之間微妙但重要的區(qū)別,關(guān)于該主題可以從拓展閱讀獲取更多內(nèi)容。
文中的樣例代碼可以從 GitHub.獲取。
參考
[1] Guide To Java 8 Optional
[2] Java 8 Optional
[3] Java 11 Optional