Java8中Optional的一些常見(jiàn)錯(cuò)誤用法總結(jié)
前言
Java 8 引入的 Optional 類(lèi)型,基本是把它當(dāng)作 null 值優(yōu)雅的處理方式。其實(shí)也不完全如此,Optional 在語(yǔ)義上更能體現(xiàn)有還是沒(méi)有值。所以它不是設(shè)計(jì)來(lái)作為 null 的替代品,如果方法返回 null 值表達(dá)了二義性,沒(méi)有結(jié)果或是執(zhí)行中出現(xiàn)異常。
在 Oracle 做 Java 語(yǔ)言工作的 Brian Goetz 在 Stack Overflow 回復(fù) Should Java 8 getters return optional type? 中講述了引入 Optional 的主要?jiǎng)訖C(jī)。
Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”, and using null for such was overwhelmingly likely to cause errors.
說(shuō)的是 Optional 提供了一個(gè)有限的機(jī)制讓類(lèi)庫(kù)方法返回值清晰的表達(dá)有與沒(méi)有值,避免很多時(shí)候 null 造成的錯(cuò)誤。并非有了 Optional 就要完全杜絕 NullPointerException。
在 Java 8 之前一個(gè)實(shí)踐是方法返回集合或數(shù)組時(shí),應(yīng)返回空集合或數(shù)組表示沒(méi)有元素; 而對(duì)于返回對(duì)象,只能用 null 來(lái)表示不存在,現(xiàn)在可以用 Optional 來(lái)表示這個(gè)意義。
自 Java8 于 2014-03-18 發(fā)布后已 5 年有余,這里就列舉幾個(gè)我們?cè)陧?xiàng)目實(shí)踐中使用 Optional 常見(jiàn)的幾個(gè)用法。
Optional 類(lèi)型作為字段或方法參數(shù)
這兒把 Optional 類(lèi)型用為字段(類(lèi)或?qū)嵗兞?和方法參數(shù)放在一起來(lái)講,是因?yàn)榧偃缥覀兪褂?IntelliJ IDEA 來(lái)寫(xiě) Java 8 代碼,IDEA 對(duì)于 Optional 作為字段和方法參數(shù)會(huì)給出同樣的代碼建議:
Reports any uses of java.util.Optional<T> , java.util.OptionalDouble , java.util.OptionalInt , java.util.OptionalLong or com.google.common.base.Optional as the type for a field or parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable , which java.util.Optional is not.
不建議用任何的 Optional 類(lèi)型作為字段或參數(shù),Optional 設(shè)計(jì)為有限的機(jī)制讓類(lèi)庫(kù)方法返回值清晰的表達(dá) "沒(méi)有值"。 Optional 是不可被序列化的,如果類(lèi)是可序列化的就會(huì)出問(wèn)題。
上面其實(shí)重復(fù)了 Java 8 引入 Optional 的意圖,我們還有必要繼續(xù)深入理解一下為什么不該用 Optional 作為字段或方法參數(shù)。
當(dāng)我們選擇 Optional 類(lèi)型而非內(nèi)部包裝的類(lèi)型后,應(yīng)該是假定了該 Optional 類(lèi)型不為 null,否則我們?cè)谑褂?Optional 字段或方法參數(shù)時(shí)就變得復(fù)雜了,需要進(jìn)行兩番檢查。
public class User {
private String firstName;
private Optional<String> middleName = Optional.empty();
private String lastName;
public void setMiddleName(Optional<String> middleName) {
this.middleName = middleName;
}
public String getFullName() {
String fullName = firstName;
if(middleName != null) {
if(middleName.isPresent()){
fullName = fullName.concat("." + middleName.get());
}
return fullName.concat("." + lastName);
}
}
由于 middleName 的 setter 方法,我們可能造成 middleName 變?yōu)?null 值,所以在構(gòu)建 fullName 時(shí)必須兩重檢查。
并且在調(diào)用 setMiddleName(...) 方法時(shí)也有些累贅了
user.setMiddleName(Optional.empty());
user.setMiddleName(Optional.of("abc"));
而如果字段類(lèi)型非 Optional 類(lèi)型,而傳入的方法參數(shù)為 Optional 類(lèi)型,要進(jìn)行賦值的話
private String middleName;
public void updateMiddleName(Optional<String> middleName) {
if(middleName != null) {
this.middleName = middleName.orElse(null);
} else {
this.middleName = null;
}
}
前面兩段代碼如果應(yīng)用 Optional.ofNullable(...) 包裹 Optional 來(lái)替代 if(middleName != null) 就更復(fù)雜了。
對(duì)于本例直接用 String 類(lèi)型的 middleName 作為字段或方法參數(shù)就行,null 值可以表達(dá)沒(méi)有 middleName。如果不允許 null 值 middleName, 顯式的進(jìn)行入口參數(shù)檢查而拒絕該輸入 -- 拋出異常。
利用 Optional 過(guò)度檢查方法參數(shù)
這一 Optional 的用法與之前的可能為 null 值的方法參數(shù),不分清紅皂白就用 if...else 檢查,總有一種不安全感,步步驚心,結(jié)果可能事與愿違。
public User getUserById(String userId) {
if(userId != null) {
return userDao.findById(userId);
} else {
return null;
}
}
只是到了 Java 8 改成了用 Optional
return if(Optional.ofNullable(userId) .map(id -> userDao.findById(id)) .orElse(null);
上面兩段代碼其實(shí)是同樣的問(wèn)題,如果輸入的 userId 是 null 值不調(diào)用 findById(...) 方法而直接返回 null 值,這就有兩個(gè)問(wèn)題
userDao.findById(...) getUserById(userId)
這種情況下立即拋出 NullPointerException 是一個(gè)更好的主意,參考下面的代碼
public User getUserById(String userId) { //拋出出 NullPointerException 如果 null userId
return userDao.findById(Objects.requireNoNull(userId, "Invalid null userId");
}
//or
public User getUserById(String userId) { //拋出 IllegalArgumentException 如果 null userId
Preconditions.checkArgument(userId != null, "Invalid null userId");
return userDao.findById(userId);
}
即使用了 Optional 的 orElseThrow 拋出異常也不能明確異常造成的原因,比如下面的代碼
public User getUserById(String userId) {
return Optional.ofNullable(userId)
.map(id -> userDao.findById(id))
orElseThrow(() ->
new RuntimeException("userId 是 null 或 findById(id) 返回了 null 值"));
}
糾正辦法是認(rèn)真的審視方法的輸入?yún)?shù),對(duì)不符合要求的輸入應(yīng)立即拒絕,防止對(duì)下層的壓力與污染,并報(bào)告出準(zhǔn)確的錯(cuò)誤信息,以有利于快速定位修復(fù)。
Optional.map(...) 中再次 null 值判斷
假如有這樣的對(duì)象導(dǎo)航關(guān)系 user.getOrder().getProduct().getId() , 輸入是一個(gè) user 對(duì)象
String productId = Optional.ofNullable(user)
.map(User::getOrder)
.flatMap(order -> Optional.ofNullable(order.getProduct())) //1
.flatMap(product -> Optional.ofNullable(product.getId())) //2
.orElse("");
#1 和 #2 中應(yīng)用 flatMap 再次用 Optional.ofNullable() 是因?yàn)閾?dān)心 order.getProduct() 或 product.getId() 返回了 null 值,所以又用 Optional.ofNullable(...) 包裹了一次。代碼的執(zhí)行結(jié)果仍然是對(duì)的,代碼真要這么寫(xiě)的話真是 Oracle 的責(zé)任。這忽略了 Optional.map(...) 的功能,只要看下它的源代碼就知道
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
map(...) 函數(shù)中已有考慮拆解后的 null 值,因此呢 flatMap 中又 Optional.ofNullable 是多余的,只需簡(jiǎn)單一路用 map(...) 函數(shù)
String productId = Optional.ofNullable(user)
.map(User::getOrder)
.map(order -> order.getProduct()) //1
.map(product -> product.getId()) //2
.orElse("");
Optional.ofNullable 應(yīng)用于明確的非 null 值
如果有時(shí)候只需要對(duì)一個(gè)明確不為 null 的值進(jìn)行 Optional 包裝的話,就沒(méi)有必要用 ofNullable(...) 方法,例如
public Optional<User> getUserById(String userId) {
if("ADMIN".equals(userId)) {
User adminUser = new User("admin");
return Optional.ofNullable(adminUser); //1
} else {
return userDao.findById(userId);
}
}
在代碼 #1 處非常明確 adminUser 是不可能為 null 值的,所以應(yīng)該直接用 Optional.of(adminUser) 。這也是為什么 Optional 要聲明 of(..) 和 ofNullable(..) 兩個(gè)方法。看看它們的源代碼:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
知道被包裹的值不可能為 null 時(shí)調(diào)用 ofNullable(value) 多了一次多余的 null 值檢查。相應(yīng)的對(duì)于非 null 值的字面常量
Optional.ofNullable(100); //這樣不好 Optional.of(100); //應(yīng)該這么用
小結(jié):
- 要理解 Optional 的設(shè)計(jì)用意,所以語(yǔ)意上應(yīng)用它來(lái)表達(dá)有/無(wú)結(jié)果,不適于作為類(lèi)字段與方法參數(shù)
- 傾向于方法返回單個(gè)對(duì)象,用 Optional 類(lèi)型表示無(wú)結(jié)果以避免 null 值的二義性
- Optional 進(jìn)行方法參數(shù)檢查不能掩蓋了錯(cuò)誤,最好是明確非法的參數(shù)輸入及時(shí)拋出輸入異常
- 對(duì)于最后兩種不正確的用法應(yīng)熟悉 Optional 的源代碼實(shí)現(xiàn)就能規(guī)避
鏈接:
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
SpringBoot整合MyBatis四種常用的分頁(yè)方式(詳細(xì)總結(jié))
這篇文章詳細(xì)給大家總結(jié)了SpringBoot整合MyBatis四種常用的分頁(yè)方式,文中通過(guò)代碼示例為大家介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
Java時(shí)區(qū)轉(zhuǎn)換實(shí)例代碼解析
這篇文章主要介紹了Java時(shí)區(qū)轉(zhuǎn)換實(shí)例代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
IDEA:Git stash 暫存分支修改的實(shí)現(xiàn)代碼
這篇文章主要介紹了IDEA:Git stash 暫存分支修改的實(shí)現(xiàn)代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
基于Java實(shí)現(xiàn)本地APK文件搜索與HTTP下載服務(wù)
在移動(dòng)應(yīng)用開(kāi)發(fā)和測(cè)試過(guò)程中,經(jīng)常需要從本地服務(wù)器獲取最新的 APK 安裝包,本文將詳細(xì)介紹如何使用 Java 構(gòu)建一個(gè)能夠搜索本地最新 APK 文件并通過(guò) HTTP 請(qǐng)求提供下載服務(wù)的應(yīng)用2025-07-07
Windows10系統(tǒng)下修改jar中的文件并重新打包成jar文件然后運(yùn)行的操作步驟
這篇文章主要介紹了Windows10系統(tǒng)下修改jar中的文件并重新打包成jar文件然后運(yùn)行的操作步驟,文中通過(guò)圖文結(jié)合的形式給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-08-08

