本文獲得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