了解、接受和利用Java中的Optional (類)

本文獲得Stackify授權(quán)翻譯發(fā)表,轉(zhuǎn)載需要注明來自公眾號EAWorld。


作者:EUGEN PARASCHIV

譯者:海松

原題: Understanding, Accepting and Leveraging Optional in Java


編者按:Java 9終于在9月21號發(fā)布,于是知乎上關(guān)于“現(xiàn)在Java初學用等Java9出來再學嗎”之類的問題可能有更新。在 Java 8 引入Optional特性的基礎(chǔ)上,Java 9 又為 Optional 類增加了三種方法:or()、ifPresentOrElse() 和 stream(),本文的最后,也針對這些新特性做了一些說明和實例,希望有助于大家理解。


1.概述


Java 8 最有趣的特性之一,就是引入了全新的 Optional 類。該類主要用來處理幾乎每位程序員都碰到過的麻煩問題—— 空指針異常(NullPointerException)。


從本質(zhì)上來說,該類屬于包含可選值的封裝類(wrapper class),因此它既可以包含對象也可以僅僅為空。


伴隨著 Java函數(shù)式編程方式的異軍突起,Optional 應運而生,除了可助該編程方式一臂之力外,Optional 的作用顯然還遠不止于此。


我們先來看一個簡單的案例。在 Java 8 之前,凡涉及到訪問對象方法或者對象屬性的操作,無論數(shù)量多寡,都可能導致 空指針異常:


String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();


假如我們想保證上面的小示例不出現(xiàn)異常,我們可能需要在訪問它之前對每一個值進行顯式檢查:

if (user != null) {
 ? ?Address address = user.getAddress();
 ? ?if (address != null) {
 ? ? ? ?Country country = address.getCountry();
 ? ? ? ?if (country != null) {
 ? ? ? ? ? ?String isocode = country.getIsocode();
 ? ? ? ? ? ?if (isocode != null) {
 ? ? ? ? ? ? ? ?isocode = isocode.toUpperCase();
 ? ? ? ? ? ?}
 ? ? ? ?}
 ? ?}
}


這么一來,會讓代碼顯得累贅而難以維護。


為簡化這一過程,我們將使用 Optional 類取代上述代碼,從創(chuàng)建和驗證一個實例開始,再到使用其提供的不同方法,最后將其和返回相同類型的其他方法進行組合,而最后這項組合功能正是 Optional 的真正強大之處。


2.創(chuàng)建 Optional ?實例


為了實現(xiàn)重復迭代(reiterate),該類型對象既可以包含一個值,也可以為空。我們先用具有相同名稱的方法來創(chuàng)建一個空 Optional:

@Test(expected = NoSuchElementException.class)
public void whenCreateEmptyOptional_thenNull() { ? ?Optional<User> emptyOpt = Optional.empty(); ? ?emptyOpt.get();
}


毫無疑問,如果您要訪問 emptyOpt ?變量的值,會導致 NoSuchElementException異常。


您可以用 of() 和 ofNullable(),來創(chuàng)建包含一個值的Optional 對象。兩種方法的區(qū)別在于:如果你將 null 值作為參數(shù)傳入 of() 方法,那么該方法會拋出一個 空指針異常。

@Test(expected = NullPointerException.class)public void whenCreateOfEmptyOptional_thenNullPointerException() {
 ? ?Optional<User> opt = Optional.of(user);
}

如你所見,空指針 異常的問題并沒有得到徹底解決。因此,只有當對象不為 null 時, of()的方法才可行。


如果對象既可能為 null ,也可能為非 null ,就必須選擇 ofNullable()。

Optional<User> opt = Optional.ofNullable(user);


訪問 Optional 對象的值


想要獲取Optional實例內(nèi)部的對象,方法之一是使用get()方法

@Test
public void whenCreateOfNullableOptional_thenOk() { ? ?String name = "John"; ? ?Optional<String> opt = Optional.ofNullable(name); ? ? ? ?assertEquals("John", opt.get());
}


但和之前類似,這種方法在值為 null 時也會拋出異常。為避免出現(xiàn)異常,您可選擇首先檢驗其中是否存在值。

@Test
public void whenCheckIfPresent_thenOk() { ? ?User user = new User("john@gmail.com", "1234"); ? ?Optional<User> opt = Optional.ofNullable(user); ? ?assertTrue(opt.isPresent()); ? ?assertEquals(user.getEmail(), opt.get().getEmail());
}


利用 ifPresent()也可以用來檢查是否存在值。而且該方法還帶有一個 Consumer 參數(shù),在對象不為空時執(zhí)行 λ 表達式:

opt.ifPresent( u -> assertEquals(user.getEmail(), u.getEmail()));


在此示例中,只有在用戶對象非空時,才會執(zhí)行assertion。


接下來,我們看看能夠替換空值的各種方法。


返回默認值


Optional 類提供了一些 API,用于返回對象值或在對象為空時返回默認值。

其中的第一種方法是 orElse(),它的工作方式相當直接:如果存在值,則返回該值,如果不存在值,則返回它收到的參數(shù):

@Test
public void whenEmptyValue_thenReturnDefault() { ? ?User user = null; ? ?User user2 = new User("anna@gmail.com", "1234"); ? ?User result = Optional.ofNullable(user).orElse(user2); ? ?assertEquals(user2.getEmail(), result.getEmail());
}


此處,user 對象為空,所以 user2 作為默認替代值返回。


如果對象的初始值不為空,則默認值會被忽略:

@Test
public void whenValueNotNull_thenIgnoreDefault() { ? ?User user = new User("john@gmail.com","1234"); ? ?User user2 = new User("anna@gmail.com", "1234"); ? ?User result = Optional.ofNullable(user).orElse(user2); ? ?assertEquals("john@gmail.com", result.getEmail());
}


第二種同類 API 是 orElseGet() ——其工作方式略有不同。在本例中,如果存在值,則方法回返該值,如果不存在,則其執(zhí)行 Supplier 函數(shù)接口(作為其收到的一個參數(shù)),并返回執(zhí)行結(jié)果:

User result = Optional.ofNullable(user).orElseGet( () -> user2);


orElse() 和 orElseGet() 之間的區(qū)別


乍一看,兩種方法似乎效果相同。但實際還是有差別。我們可以通過創(chuàng)建幾個例子,來看看二者在功能表現(xiàn)上的相似處和不同點。


首先,我們來看對象為空時,二者的表現(xiàn):

@Test
public void givenEmptyValue_whenCompare_thenOk() { ? ?User user = null ? ?logger.debug("Using orElse"); ? ?User result = Optional.ofNullable(user).orElse(createNewUser()); ? ?logger.debug("Using orElseGet"); ? ?User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}

private User createNewUser() { ? ?logger.debug("Creating New User"); ? ?return new User("extra@gmail.com", "1234");
}


在上面的代碼中,兩種方法都調(diào)用了createNewUser() ?方法,后者會記錄消息日志并返回 User 對象。


代碼輸出如下:

Using orElse
Creating New User
Using orElseGet
Creating New User


可見,當對象為空時,二者在表現(xiàn)上并無差別,都是代之以返回默認值。


接下來,我們舉一個 Optional 不為空時的相似例子:

@Test
public void givenPresentValue_whenCompare_thenOk() { ? ?User user = new User("john@gmail.com", "1234"); ? ?logger.info("Using orElse"); ? ?User result = Optional.ofNullable(user).orElse(createNewUser()); ? ?logger.info("Using orElseGet"); ? ?User result2 = Optional.ofNullable(user).orElseGet(() -> createNewUser());
}


這次的輸出如下:

Using orElse
Creating New User
Using orElseGet

此處,兩個 Optional 對象都包含有一個非空值,而兩種方法都會將其作為返回值。但是,orElse() 方法仍然會創(chuàng)建默認的 User 對象。相反,orElseGet() ?方法將不再創(chuàng)建 User 對象。


當操作中包含大量密集調(diào)用時,比如 web 服務調(diào)用或者數(shù)據(jù)庫查詢,這種差別就會對代碼執(zhí)行產(chǎn)生重大影響。


返回異常


除了 orElse() 和 orElseGet() 方法,Optional還定義了 ElseThrow() API,其作用是在對象為空時,直接拋出一個異常,而不是返回一個替代值。

@Test(expected = IllegalArgumentException.class)
public void whenThrowException_thenOk() { ? ?User result = Optional.ofNullable(user) ? ? ?.orElseThrow( () -> new IllegalArgumentException());
}


此處,如果 user 值為空,則會拋出 非法參數(shù)異常。


這讓我們可以從更多靈活的語義中挑選所要拋出的異常,而不是千篇一律的 空指針異常。


既然我們已對 Optional 本身的使用有了一定了解,那就讓我們再來看看用于轉(zhuǎn)換和過濾 Optional 值的其他方法。


3.對值進行轉(zhuǎn)換


Optional 值可通過多種方法進行轉(zhuǎn)換;我們就從 map() 和 flatMap() 說起。


首先,讓我們看個使用 map() API 的例子:

@Test
public void whenMap_thenOk() { ? ?User user = new User("anna@gmail.com", "1234"); ? ?String email = Optional.ofNullable(user) ? ? ?.map(u -> u.getEmail()).orElse("default@gmail.com"); ? ? ? ?assertEquals(email, user.getEmail());
}


Map() 將 Function 參數(shù)作為值,然后返回 Optional 中經(jīng)過封裝的結(jié)果。這將使我們可以在后續(xù)附加一些操作,比如此處的 orElse() 。


相比之下,flatMap() 也是將 Function 參數(shù)作為 Optional 值,但它后面是直接返回結(jié)果。


為了查看實際效果,我們添加一個方法,可向 User ?類返回 Optional:

public class User { ? ?
 ? ?private String position;

 ? ?public Optional<String> getPosition() {
 ? ? ? ?return Optional.ofNullable(position);
 ? ?}
 ? ?
 ? ?//...
}


因為 getter 方法返回一個 Optional 字符串值,在請求Optional User 對象時,您可將其作為 flatMap() 的參數(shù)。返回值為非封裝字符串值:

@Test
public void whenFlatMap_thenOk() { ? ?User user = new User("anna@gmail.com", "1234"); ? ?user.setPosition("Developer"); ? ?String position = Optional.ofNullable(user) ? ? ?.flatMap(u -> u.getPosition()).orElse("default"); ? ? ? ?assertEquals(position, user.getPosition().get());
}


4.對值進行過濾


除了對值進行轉(zhuǎn)換的功能,Optional 類還提供了根據(jù)條件對值進行“過濾”的功能。


filter() 方法將 predicate 作為參數(shù),當測試評估為真時,返回實際值。否則,當測試為假時,返回值則為空 Optional。


我們來看一個例子——基于非?;镜碾娮余]件驗證,接受或者拒絕 User:

@Test
public void whenFilter_thenOk() { ? ?User user = new User("anna@gmail.com", "1234"); ? ?Optional<User> result = Optional.ofNullable(user) ? ? ?.filter(u -> u.getEmail() != null && u.getEmail().contains("@")); ? ? ? ?assertTrue(result.isPresent());
}


作為通過過濾測試的結(jié)果,Result 對象將包含一個非 null 值。


5.對 Optional 類的方法進行鏈接


Optional 還具有更多強大的應用,鑒于絕大多數(shù) Optional 方法會返回相同類型的對象,您可以將它們的不同組合鏈接起來。


我們把示例代碼重新寫一下。


首先,我們重構(gòu)這些類,這樣 getter 方法將返回 Optional 引用(references):

public class User {
 ? ?private Address address;

 ? ?public Optional<Address> getAddress() {
 ? ? ? ?return Optional.ofNullable(address);
 ? ?}

 ? ?// ...
}
public class Address {
 ? ?private Country country;
 ? ?
 ? ?public Optional<Country> getCountry() {
 ? ? ? ?return Optional.ofNullable(country);
 ? ?}

 ? ?// ...
}


上述結(jié)構(gòu)可用嵌套集合來直觀地表示:



現(xiàn)在刪除對 null 進行檢查的代碼,并以 Optional 方法來取代:

@Test
public void whenChaining_thenOk() { ? ?User user = new User("anna@gmail.com", "1234"); ? ?String result = Optional.ofNullable(user) ? ? ?.flatMap(u -> u.getAddress()) ? ? ?.flatMap(a -> a.getCountry()) ? ? ?.map(c -> c.getIsocode()) ? ? ?.orElse("default"); ? ?assertEquals(result, "default");
}


上面的代碼可通過方法引用(method references)做進一步精簡:

String result = Optional.ofNullable(user)
 ?.flatMap(User::getAddress)
 ?.flatMap(Address::getCountry)
 ?.map(Country::getIsocode)
 ?.orElse("default");


從現(xiàn)在的結(jié)果看,代碼比先前冗長的條件驅(qū)動(conditional-driven)版本要簡潔許多。


6.Java 9 新增特性


在 Java 8 引入Optional特性的基礎(chǔ)上,Java 9 又為 Optional 類增加了三種方法:or()、ifPresentOrElse() 和 stream()。


在某種意義上,or() 方法同 orElse() 和 orElseGet() 類似,都是在對象為空時提供替換功能。在本例中,返回值為另一個由 Supplier 參數(shù)生成的 Optional 對象。


如果對象包含一個值,則λ表達式不會執(zhí)行:

@Test
public void whenEmptyOptional_thenGetValueFromOr() { ? ?User result = Optional.ofNullable(user) ? ? ?.or( () -> Optional.of(new User("default","1234"))).get(); ? ? ? ? ? ? ? ? ? ?assertEquals(result.getEmail(), "default");
}


在上述示例中,如果 ?user ?變量為空,則將返回包含一個帶有電子郵件“default”的 User ?對象的 Optional 。


?ifPresentOrElse() 方法帶有兩個參數(shù):Consumer ?和 Runnable。如果對象包含一個值,則會執(zhí)行 Consumer ?動作;否則,會執(zhí)行 Runnable ?動作。


如果您希望使用某個現(xiàn)有值執(zhí)行一個動作,或者僅僅想跟蹤某個值是否已作定義,則該方法非常有用:

Optional.ofNullable(user).ifPresentOrElse( u -> logger.info("User is:" + u.getEmail()),
 ?() -> logger.info("User not found"));


最后,您可從新 stream() 方法的擴展 Stream API 得到益處,具體做法是將實例轉(zhuǎn)換為一個 Stream 對象。如果 Optional 不存在值,則 Stream 為空,如果 Optional 包含一個非 null 值,則 Stream 會包含單個值。


我們舉個將 Optional 作為 Stream 處理的例子:

@Test
public void whenGetStream_thenOk() { ? ?User user = new User("john@gmail.com", "1234"); ? ?List<String> emails = Optional.ofNullable(user) ? ? ?.stream() ? ? ?.filter(u -> u.getEmail() != null && u.getEmail().contains("@")) ? ? ?.map( u -> u.getEmail()) ? ? ?.collect(Collectors.toList()); ? ? ?assertTrue(emails.size() == 1); ? ?assertEquals(emails.get(0), user.getEmail());
}


在此處使用 Stream ,使得應用 filter()、map() 和 collect() 等 Stream 接口方法 來獲取 List 成為可能。


7.應該如何使用 Optional


在使用 Optional 時,我們需要考慮幾個問題,來決定什么時候用以及如何用。


第一個要點,Optional 并不能序列化(Serializable )。因此,它不可以在類中當作一個字段(field)來使用。


如果您需要序列化一個包含 Optional 值的對象,Jackson library(https://stackify.com/java-xml-jackson/)可支持將 Optionals當作普通對象來對待。這意味著,Jackson 會將空對象作為 null,它還會將有值對象當作一個包含該值的字段。這個功能可在 jackson-modules-java8 (https://github.com/FasterXML/jackson-modules-java8) 項目中找到。


另一種不太適合使用該類型的情況,是將該類型作為方法或者構(gòu)造函數(shù)的參數(shù)。這將導致不必要的代碼復雜化。

User user = new User("john@gmail.com", "1234", Optional.empty());


相反,使用方法重載(method overloading)來處理非強制性參數(shù)要方便得多。


Optional的主要用途是作為一種返回類型。在獲得該類型的一個實例后,如果存在值,您可以提取該值,如果不存在值,則您可以獲得一個替換值。


Optional類對我們最有幫助的一個用例,是其同 stream 或者其他方法的組合使用,這些方法會返回一個可構(gòu)建流暢 API 的Optional 值。


我們舉個使用 Stream findFirst() 方法并返回 Optional 對象的例子:

@Test
public void whenEmptyStream_thenReturnDefaultOptional() { ? ?List<User> users = new ArrayList<>(); ? ?User user = users.stream().findFirst().orElse(new User("default", "1234")); ? ? ? ?assertEquals(user.getEmail(), "default");
}


8.總結(jié)


對于 Java 語言來說,Optional 是一項非常有用的新增特性。盡管無法徹底消除 空指針異常,但 Optional 可以最大限度減少代碼執(zhí)行過程中出現(xiàn)的此類異常。


同時,該類經(jīng)過精心設(shè)計,對于 Java 8 加入的新函數(shù)式支持(functional support)而言,它自然而然地成為非同一般的新增特性。


總之,該類簡單而不失強大,相比之前的同類功能,用其編寫代碼既簡單易讀又不易出錯。


原文鏈接https://stackify.com/optional-java/


關(guān)于作者

Eugen是一名軟件工程師,對Spring、REST API、安全和教育擁有極大熱情。同時,他還是Baeldung(推特賬號@baeldung)的創(chuàng)始人。


關(guān)于EAWorld

微服務,DevOps,元數(shù)據(jù),企業(yè)架構(gòu)原創(chuàng)技術(shù)分享EAii(Enterprise?Architecture?Innovation?Institute)企業(yè)架構(gòu)創(chuàng)新研究院旗下官方微信公眾號。


微信號:eaworld,長按二維碼關(guān)注


8月-9月,PWorld系列技術(shù)趴還將繼續(xù)上演。目前,10月28日將在北京舉行PWorld MeetUP“移動平臺新技術(shù)發(fā)展新趨勢及企業(yè)實踐”已啟動報名,戳“閱讀原文”可直達報名頁面,并了解更多詳情~


閱讀原文:http://mp.weixin.qq.com/s?timestamp=1507543888&src=3&ver=1&signature=UZ8tr00*SsuDn00EmfHtU1-xiFhuukYS4p5y6T-Are8St7NyWyF*9tS*xa9ANqKHKVPDlsCZ534ofloKyemZBJbb7W1nG9csgIh31SPMAOKSeAZ*ZMQFblNgL6A9cXFtMHwhPAZRopoP2-Ekysi2JtNfqchrWo1Z*cwi04eg2PM=&devicetype=Windows-QQBrowser&version=61030004&pass_ticket=qMx7ntinAtmqhVn+C23mCuwc9ZRyUp20kIusGgbFLi0=&uin=MTc1MDA1NjU1&ascene=1
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • Optional 本章內(nèi)容 如何為缺失的值建模 Optional 類 應用Optional的幾種模式 使用Opti...
    追憶逝水年華閱讀 1,804評論 0 0
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,707評論 18 399
  • 太累 累 沒有任何休息 還有有一個人陪伴我 感激她
    凱燊閱讀 186評論 0 2
  • 最近,一首歌“成都”,讓成都享譽大江南北,沒錯,我就是來自成都的一只猴。 2年前,我在川師畢業(yè)了,由于研究生沒考上...
    二西子閱讀 3,809評論 3 2