NPE(NullPointerException)是我們代碼工作中最常遇到的一個異常,非常的難受,如何優雅的處理它呢。
本篇文章將詳細介紹Optional類,以及如何用它消除代碼中的null檢查。Optional是爲了防止NullPointerException,使代碼更優雅。
避免使用null
檢查
作爲Java開發人員,幾乎所有人都遇到過NullPointerException
異常,大多數人遇到NullPointerException
異常時都會在異常出現的地方加上if
代碼塊來判斷值不爲空,比如下面的代碼:
public void bindUserToRole(User user) {
if (user != null) {
String roleId = user.getRoleId();
if (roleId != null) {
Role role = roleDao.findOne(roleId);
if (role != null) {
role.setUserId(user.getUserId());
roleDao.save(role);
}
}
}
}
這是比較普遍的做法,爲了避免出現NullPointerException
異常,手動對可能爲null
值進行了處理,不過代碼看起來非常糟糕,業務邏輯被淹沒在if
邏輯判斷中,也許下面的代碼看起來可讀性稍好一些:
public String bindUserToRole(User user) {
if (user == null) {
return;
}
String roleId = user.getRoleId();
if (roleId == null) {
return;
}
Role = roleDao.findOne(roleId);
if (role != null) {
role.setUserId(user.getUserId());
roleDao.save(role);
}
}
上面的代碼避免了深層的if
語句嵌套,但本質上是一樣的,方法內有三個不同的返回點,出錯後調試也不容易,因爲你不知道是那個值導致了NullPointerException
異常。
基於上面的原因,Java 8中引入了一個新的類Optional
,用以避免使用null
值引發的種種問題。
Optional
類
java.util.Optional<T>
類是一個封裝了Optional
值的容器對象,Optional
值可以爲null
,如果值存在,調用isPresent()
方法返回true
,調用get()
方法可以獲取值。
創建Optional
對象
Optional
類提供類三個方法用於實例化一個Optional
對象,它們分別爲empty()
、of()
、ofNullable()
,這三個方法都是靜態方法,可以直接調用。
empty()
方法用於創建一個沒有值的Optional
對象:
Optional<String> emptyOpt = Optional.empty();
empty()
方法創建的對象沒有值,如果對emptyOpt
變量調用isPresent()
方法會返回false
,調用get()
方法拋出NullPointerException
異常。
of()
方法使用一個非空的值創建Optional
對象:
String str = "Hello World";
Optional<String> notNullOpt = Optional.of(str);
ofNullable()
方法接收一個可以爲null
的值:
Optional<String> nullableOpt = Optional.ofNullable(str);
如果str
的值爲null
,得到的nullableOpt
是一個沒有值的Optional
對象。
提取Optional
對象中的值
如果我們要獲取User
對象中的roleId
屬性值,常見的方式是直接獲取:
String roleId = null;
if (user != null) {
roleId = user.getRoleId();
}
使用Optional
中提供的map()
方法可以以更簡單的方式實現:
Optional<User> userOpt = Optional.ofNullable(user);
Optional<String> roleIdOpt = userOpt.map(User::getRoleId);
使用orElse()
方法獲取值
Optional
類還包含其他方法用於獲取值,這些方法分別爲:
orElse()
:如果有值就返回,否則返回一個給定的值作爲默認值;orElseGet()
:與orElse()
方法作用類似,區別在於生成默認值的方式不同。該方法接受一個Supplier<? extends T>
函數式接口參數,用於生成默認值;orElseThrow()
:與前面介紹的get()
方法類似,當值爲null
時調用這兩個方法都會拋出NullPointerException
異常,區別在於該方法可以指定拋出的異常類型。
下面來看看這三個方法的具體用法:
String str = "Hello World";
Optional<String> strOpt = Optional.of(str);
String orElseResult = strOpt.orElse("Hello Shanghai");
String orElseGet = strOpt.orElseGet(() -> "Hello Shanghai");
String orElseThrow = strOpt.orElseThrow(
() -> new IllegalArgumentException("Argument 'str' cannot be null or blank."));
此外,Optional
類還提供了一個ifPresent()
方法,該方法接收一個Consumer<? super T>
函數式接口,一般用於將信息打印到控制檯:
Optional<String> strOpt = Optional.of("Hello World");
strOpt.ifPresent(System.out::println);
使用filter()
方法過濾
filter()
方法可用於判斷Optional
對象是否滿足給定條件,一般用於條件過濾:
Optional<String> optional = Optional.of("[email protected]");
optional = optional.filter(str -> str.contains("164"));
在上面的代碼中,如果filter()
方法中的Lambda表達式成立,filter()
方法會返回當前Optional
對象值,否則,返回一個值爲空的Optional
對象。
如何正確使用Optional
通過上面的例子可以看出,Optional
類可以優雅的避免NullPointerException
帶來的各種問題,不過,你是否真正掌握了Optional
的用法?假設你試圖使用Optional
來避免可能出現的NullPointerException
異常,編寫了如下代碼:
Optional<User> userOpt = Optional.ofNullable(user);
if (userOpt.isPresent()) {
User user = userOpt.get();
// do something...
} else {
// do something...
}
坦白說,上面的代碼與我們之前的使用if
語句判斷空值沒有任何區別,沒有起到Optional
的正真作用:
if (user != null) {
// do something...
} else {
// do something...
}
當我們從之前版本切換到Java 8的時候,不應該還按照之前的思維方式處理null
值,Java 8提倡函數式編程,新增的許多API都可以用函數式編程表示,Optional
類也是其中之一。這裏有幾條關於Optional
使用的建議:
- 儘量避免在程序中直接調用
Optional
對象的get()
和isPresent()
方法; - 避免使用
Optional
類型聲明實體類的屬性;
第一條建議中直接調用get()
方法是很危險的做法,如果Optional
的值爲空,那麼毫無疑問會拋出NullPointerException
異常,而爲了調用get()
方法而使用isPresent()
方法作爲空值檢查,這種做法與傳統的用if
語句塊做空值檢查沒有任何區別。
第二條建議避免使用Optional
作爲實體類的屬性,它在設計的時候就沒有考慮過用來作爲類的屬性,如果你查看Optional
的源代碼,你會發現它沒有實現java.io.Serializable
接口,這在某些情況下是很重要的(比如你的項目中使用了某些序列化框架),使用了Optional
作爲實體類的屬性,意味着他們不能被序列化。
下面我們通過一些例子講解Optional
的正確用法:
正確創建Optional
對象
上面提到創建Optional
對象有三個方法,empty()
方法比較簡單,沒什麼特別要說明的。主要是of()
和ofNullable()
方法。當你很確定一個對象不可能爲null
的時候,應該使用of()
方法,否則,儘可能使用ofNullable()
方法,比如:
public static void method(Role role) {
// 當Optional的值通過常量獲得或者通過關鍵字new初始化,可以直接使用of()方法
Optional<String> strOpt = Optional.of("Hello World");
Optional<User> userOpt = Optional.of(new User());
// 方法參數中role值不確定是否爲null,使用ofNullable()方法創建
Optional<Role> roleOpt = Optional.ofNullable(role);
}
orElse()
方法的使用
return str != null ? str : "Hello World"
上面的代碼表示判斷字符串str
是否爲空,不爲空就返回,否則,返回一個常量。使用Optional
類可以表示爲:
return strOpt.orElse("Hello World")
簡化if-else
User user = ...
if (user != null) {
String userName = user.getUserName();
if (userName != null) {
return userName.toUpperCase();
} else {
return null;
}
} else {
return null;
}
上面的代碼可以簡化成:
User user = ...
Optional<User> userOpt = Optional.ofNullable(user);
return userOpt.map(User::getUserName)
.map(String::toUpperCase)
.orElse(null);
總結一下,新的Optional
類讓我們可以以函數式編程的方式處理null
值,拋棄了Java 8之前需要嵌套大量if-else
代碼塊,使代碼可讀性有了很大的提高。
坑
使用OPtional的orElse()問題
項目中有這樣一段代碼:
return Optional.ofNullable(service.A()).orElse(service.B())
功能顯而易見,service.A()如果返回值是null,則返回service.B(),否則直接返回service.A()。
實際使用中發現:
如果service.A()返回非null,最終結果是service.A(),然而service.B()這個方法也被執行了。這樣肯定就不對了,如果service.B()中還有插入數據庫或者RPC這種操作,問題就大了。剛開始還以爲是什麼執行順序問題,後來在Stack Overflow上看到老外討論orElse()和orElseGet()的區別,其中一點區別就是orElse(T)無論前面Optional容器是null還是non-null,都會執行orElse裏的方法,orElseGet(Supplier)並不會,如果service無異常拋出的情況下,Optional使用orElse或者orElseGet的返回結果都是一樣的
stack overflow上有人還給出這樣一個例子
static String B() {
System.out.println("B()...");
return "B";
}
public static void main(final String... args) {
System.out.println("-----");
System.out.println(Optional.of("A").orElse(B()));
System.out.println("-----");
System.out.println(Optional.of("A").orElseGet(() -> B()));
}
// 輸出結果
-----
B()...
A
-----
A
看了上面代碼,我就把我的代碼改成如下即可:
return Optional.ofNullable(service.A()).orElseGet(() -> service.B())
結論:Optional的orElse(T)若方法不是純計算型的,有與數據庫交互或者遠程調用的,都應該使用orElseGet()
引自:
使用Optional的orElse()問題 - liubingyu12345的博客
Java - Difference between `Optional.orElse()` and `Optional.orElseGet()` - Stack Overflow