1. 簡(jiǎn)介
本文簡(jiǎn)要介紹一下Java 8 引入的 Optional 類橘沥。引入Optional 類的主要目的是為使用可選值代替 null 提供類型級(jí)解決方案臀晃。如果觉渴,你想知道為什么需要更深入的了解和使用 Optional 類,可以參考甲骨文官方文章徽惋。
Optional 是 java.util.package 的一部分案淋,為了能夠使用,需要導(dǎo)入Optional:
import java.util.Optional;
2. 創(chuàng)建 Optional 對(duì)象
有多種方式可以創(chuàng)建 Optional 對(duì)象险绘,可以使用下面的方法創(chuàng)建一個(gè)空的 Optianal對(duì)象:
@Test
public void test_createsEmptyOptionalObject() throws Exception {
Optional<String> empty = Optional.empty();
assertFalse(empty.isPresent());
}
可以使用 isPresent API 來檢查 Optional 對(duì)象是否有封裝的值踢京,當(dāng)且僅當(dāng) * Optional* 封裝了非 null 值時(shí),API才返回 true
宦棺。
還可以使用 Optional 提供了靜態(tài)方法創(chuàng)建 Optional 對(duì)象:
@Test
public void test_createOptionalObjectWithStaticMethod() throws Exception {
String val = "not null";
Optional<String> hasVal = Optional.of(val);
assertTrue(hasVal.isPresent());
}
如果 Optional 對(duì)象有封裝的值(非 null )瓣距,可以對(duì)封裝的值進(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 對(duì)象時(shí),方法的參數(shù)不能null代咸,否則蹈丸,方法會(huì)拋出 NullPointerException:
@Test(expected = NullPointerException.class)
public void test_throwNullPointerException() throws Exception {
String val = null;
Optional<String> hasVal = Optional.of(val);
}
如果構(gòu)建 Optional 對(duì)象時(shí)可以傳入 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 對(duì)象時(shí)呐芥,如果傳入一個(gè) null 參數(shù)逻杖,方法不會(huì)拋出異常,而是返回一個(gè)空的 Optional 對(duì)象思瘟,和使用 Optional.empty API 創(chuàng)建的一樣荸百。
3. 檢查值是否存在
當(dāng)?shù)玫揭粋€(gè)從其他方法返回或自己創(chuàng)建的 Optional 對(duì)象后,可以使用isPresent API 檢查 Optional 對(duì)象是否有封裝值:
@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 對(duì)象封裝一個(gè)非空值時(shí)滨攻,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 對(duì)象封裝的值為 null 時(shí),isEmpty 返回true
铡买,其他情況返回false
更鲁。
4. 使用 ifPresent() 進(jìn)行條件處理
ifPresent API 允許我們?cè)?Optional 對(duì)象封裝的值非空時(shí)執(zhí)行一些代碼,在沒有Optional 之前奇钞,最常用的方法是使用 if 語(yǔ)句進(jìn)行判斷澡为,結(jié)果為真時(shí)執(zhí)行代碼邏輯:
if(name != null){
System.out.println(name.length);
}
這段代碼在執(zhí)行其他代碼之前先檢查 name 變量是否為 null。冗長(zhǎng)并不是這種方法的唯一問題一景埃,這種方法固有很多潛在的bug媒至。
在習(xí)慣了這種方法之后,很容易忘記在代碼的某些部分執(zhí)行空檢查谷徙。如果 null 值進(jìn)入該代碼拒啰,可能會(huì)在運(yùn)行時(shí)導(dǎo)致 NullPointerException 異常。 當(dāng)程序因輸入問題而失敗時(shí)完慧,通常是編碼不夠健壯導(dǎo)致谋旦,也是代碼實(shí)踐不好的結(jié)果。
作為強(qiáng)制執(zhí)行良好編程實(shí)踐的一種方式,Optional 可以明確地處理 null册着。 在典型的函數(shù)式編程風(fēng)格中拴孤,我們可以對(duì)實(shí)際存在的對(duì)象執(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 無封裝值時(shí)的默認(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ù)為一個(gè) Supplier 實(shí)例化漆,當(dāng) Optional 對(duì)象無封裝值時(shí),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 對(duì)象無封裝值時(shí),orElse 和 orElseGet 并無本質(zhì)上的區(qū)別锨苏,兩個(gè)API 都返回各自的默認(rèn)值疙教。但是,當(dāng) Optional 對(duì)象有封裝值時(shí)兩者有很大的區(qū)別伞租,而且兩者在性能上的差異也十分明顯贞谓。一句話總結(jié)兩者的差異就是:orElse 會(huì)觸發(fā)獲取默認(rèn)值的動(dòng)作,盡管并不需要葵诈。為了更加形象的說明裸弦,這里提供一個(gè)方法用于獲取默認(rèn)值,方法中使用 sleep 模擬這是一個(gè)耗時(shí)的操作:
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)建一個(gè)非空的 Optional 對(duì)象作喘,分別調(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é)果可以非常清晰的看出兩個(gè)API之間的差異,為了更好的性能泞坦,在編碼中優(yōu)先使用 orElseGet API 獲取 Optional 的值窖贤。。
8. 使用 orElseThrow 拋出異常
orElseThrow 與 orElse 和 orElseGet API類似贰锁,orElseThrow 提供了一種在Optional 為空時(shí)的處理方法-拋異常而不是返回默認(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* 值的最后方法(不是一個(gè)好方法):
@Test
public void test_getValueUseGet() {
Optional<String> opt = Optional.of("value");
String name = opt.get();
assertEquals("value", name);
}
和上面三種獲取值的方法不同,* get * 方法只能返回 Optional 封裝的值豌熄,如果Optional 為空授嘀,方法會(huì)拋出 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 被用于對(duì) Optional 封裝的值進(jìn)行一個(gè)內(nèi)聯(lián)測(cè)試焕济,filter API 使用一個(gè)謂詞作為參數(shù)并返回一個(gè)Optional 對(duì)象。如果盔几,被封裝的值通過測(cè)試則返回Optional 本身,否則返回一個(gè)空的 Optional 對(duì)象掩幢。
@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ù)某個(gè)預(yù)定義的規(guī)則拒絕 Optional 對(duì)象封裝的值逊拍,可以用于拒絕格式錯(cuò)誤的郵箱地址或強(qiáng)度不夠的密碼。
接下來看一個(gè)更有趣的例子(有些場(chǎng)景下不使用 Optional 為了安全的操作际邻,我們通常需要進(jìn)行多次 null 檢查)芯丧。假設(shè),我們打算購(gòu)買一部手機(jī)并且只關(guān)心手機(jī)的價(jià)格世曾。我們從手機(jī)購(gòu)買網(wǎng)站得到手機(jī)價(jià)格的推送消息缨恒,手機(jī)價(jià)格被封裝在一個(gè)對(duì)象中,數(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ī)價(jià)格是否滿足我們的預(yù)算要求的函數(shù)時(shí)(假設(shè)能接受的手機(jī)價(jià)格為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ù)真正的核心代碼僅僅是檢查價(jià)格范圍血巍,其他多余的檢查對(duì)于實(shí)現(xiàn)功能來說都是不必要的萧锉。代碼冗余可能并不是最嚴(yán)重的問題,忘記 null 檢查可能更加糟糕述寡,而這不會(huì)引發(fā)任何編譯錯(cuò)誤(代碼靜態(tài)檢查工具可以發(fā)現(xiàn)并上報(bào)告警)柿隙。
使用 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 語(yǔ)句檢查:
- 給函數(shù)出入一個(gè) null 對(duì)象,不會(huì)觸發(fā)任何錯(cuò)誤鲫凶。
- 代碼更加聚焦業(yè)務(wù)實(shí)現(xiàn)(價(jià)格檢查)禀崖,其他的事情由 Optional 負(fù)責(zé)。
11. 使用 map() 進(jìn)行值變換
在之前的章節(jié)螟炫,我們已經(jīng)看到如何使用過濾器接受或拒絕 Optional 封裝的值波附。相同的語(yǔ)法可以用于 map API 對(duì) 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 封裝了一個(gè)字符串列表叶雹,并使用 map API 對(duì) 字符串列表進(jìn)行變換,上面例子中執(zhí)行的變化是獲取字符串列表的長(zhǎng)度换吧。
map API 返回對(duì) Optional 封裝對(duì)象的計(jì)算結(jié)果折晦,最后需要調(diào)用合適的API來獲取Optional 對(duì)象的值(變換后的值)。
注意:filter API 值檢查 Optional 對(duì)象封裝的值并返回一個(gè)boolean
類型的結(jié)果,相反 map API 對(duì) Optional 對(duì)象封裝的值進(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 對(duì)密碼進(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() 對(duì)值進(jìn)行變換
和 map API 一樣宁改,我們也可以使用 flatMap API 作為一個(gè)替代方法對(duì)值進(jìn)行變換。兩者的主要區(qū)別是:map 值對(duì)未封裝的值進(jìn)行轉(zhuǎn)換魂莫,flatMap 在處理值之前先進(jìn)行“去封裝”操作还蹲,然后再執(zhí)行變換操作。
為了更清晰的解釋兩者的區(qū)別耙考,我們假設(shè)有一個(gè)Person對(duì)象谜喊,對(duì)象有三個(gè)基本屬性:名字、年齡和密碼倦始。
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)建一個(gè)Person對(duì)象斗遏,并使用 Optional 封裝創(chuàng)建的Person對(duì)象:
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é)
本文簡(jiǎn)要介紹了Java 8 Optional 類的大部分重要特性,與此同時(shí)枚碗,我們也簡(jiǎn)單闡述了為什么我們選擇使用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