提到NullPointerException
(簡稱NPE)異常,相信每個Java開發人員都不陌生,從接觸程式設計的第1天起,它就和我們如影隨形,最近處理的線上bug中,有不少都是物件沒判空導致的NullPointerException
異常。
1. 簡單回顧
引起NullPointerException
異常的地方有很多,比如呼叫String的trim()方法,比如對BigDecimal進行計算時,比如將包裝型別轉化為基本型別時,這裡簡單回顧下。
假設有個匯入模版定義如下:
package com.zwwhnly.springbootaction.model;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 匯入模版
*/
@Data
@AllArgsConstructor
public class ImportTemplate {
/**
* 模版id
*/
private int templateId;
/**
* 模版名稱
*/
private String templateName;
/**
* 模版下載url
*/
private String url;
/**
* 備註
*/
private String remark;
}
然後看下如下程式碼:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
System.out.println(importTemplate.getUrl());
}
public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
正常情況下,這段程式碼肯定是沒有問題的,但當getImportTemplateById方法返回null時,這段程式碼就會丟擲NullPointerException
異常,如下所示:
public static ImportTemplate getImportTemplateById(int id) {
return null;
}
為了程式能正常執行,就要判斷importTemplate是否為null,所以程式碼就修改為了:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}
}
專案中類似的判空程式碼應該有很多,大家可以自行看下自己專案的程式碼。
2. 使用Optional
為了避免NullPointerException
異常,JDK1.8新增了Optional類來處理空指標異常,該類位於java.util
包下,提供了一系列方法,
並且可以配合Lambda表示式一起使用,使程式碼看起來更加清晰,接下來我們看下它的使用方法。
2.1 建立例項
建立Optional例項有以下3種方式,分別為:
-
呼叫empty方法
Optional<ImportTemplate> optionalImportTemplate = Optional.empty();
-
呼叫of方法
ImportTemplate importTemplate = new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null); Optional<ImportTemplate> optionalImportTemplate = Optional.of(importTemplate);
-
呼叫ofNullable方法(推薦)
ImportTemplate importTemplate = new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null); Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
值得注意的是,當引數為null時,呼叫of方法會拋NullPointerException
異常,但呼叫ofNullable方法不會(更符合使用場景),因此推薦使用ofNullable方法:
ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.of(importTemplate);
2.2 判斷是否有值
可以呼叫isPresent
方法來判斷物件是否有值(不為null),使用方法如下所示:
ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.isPresent());
以上程式碼的輸出結果為:
ImportTemplate importTemplate = new ImportTemplate(1, "銷售訂單-普通商品匯入模版",
"o_w-140e3c1f41c94f238196539558e25bf7", null);
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.isPresent());
以上程式碼的輸出結果為:
看下isPresent
的原始碼,邏輯非常簡單,就是判斷了我們傳入的物件是否有值,即不為null:
/**
* Return {@code true} if there is a value present, otherwise {@code false}.
*
* @return {@code true} if there is a value present, otherwise {@code false}
*/
public boolean isPresent() {
return value != null;
}
2.3 獲取值
可以呼叫get
方法來獲取物件的有值,使用方法如下所示:
ImportTemplate importTemplate = new ImportTemplate(1, "銷售訂單-普通商品匯入模版",
"o_w-140e3c1f41c94f238196539558e25bf7", null);
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.get());
以上程式碼的輸出結果為:
值得注意的是,當我們傳入的物件為null時,呼叫get方法會丟擲java.util.NoSuchElementException
異常,而不是返回null。
ImportTemplate importTemplate = null;
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(importTemplate);
System.out.println(optionalImportTemplate.get());
以上程式碼的輸出結果為:
看下get
方法的原始碼,就可以知道原因:
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
2.4 先用isPresent,再用get(不推薦)
然後我們回顧下文初的程式碼:
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}
可能很多同學會把程式碼優化為下面這樣的寫法:
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
if (optionalImportTemplate.isPresent()) {
System.out.println(optionalImportTemplate.get().getUrl());
}
不推薦這麼使用,因為判斷的地方沒減少,而且還不如原來看起來清晰。
2.5 ifPresent(推薦)
那該怎麼優化呢?答案就是使用ifPresent
方法,該方法接收一個Consumer型別的引數,當值不為null時,就執行,當值為null時,就不執行,原始碼如下所示:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
優化之後的程式碼如下所示:
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
optionalImportTemplate.ifPresent(importTemplate -> System.out.println(importTemplate.getUrl()));
當然,也可以寫更多的邏輯:
Optional<ImportTemplate> optionalImportTemplate = Optional.ofNullable(getImportTemplateById(1));
optionalImportTemplate.ifPresent(importTemplate -> {
System.out.println(importTemplate.getTemplateId());
System.out.println(importTemplate.getTemplateName());
System.out.println(importTemplate.getUrl());
System.out.println(importTemplate.getRemark());
});
2.6 自定義預設值
Optional類提供了以下2個方法來自定義預設值,用於當物件為null時,返回自定義的物件:
- orElse
- orElseGet
先來看下orElse方法的使用:
public static void main(String[] args) {
ImportTemplate importTemplate = null;
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElse(getDefaultTemplate());
System.out.println(firstImportTemplate);
importTemplate = new ImportTemplate(2, "銷售訂單-不定規格商品匯入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null);
ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElse(getDefaultTemplate());
System.out.println(secondImportTemplate);
}
public static ImportTemplate getDefaultTemplate() {
System.out.println("getDefaultTemplate");
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
輸出結果:
再來看下orElseGet方法的使用:
public static void main(String[] args) {
ImportTemplate importTemplate = null;
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElseGet(() -> getDefaultTemplate());
System.out.println(firstImportTemplate);
importTemplate = new ImportTemplate(2, "銷售訂單-不定規格商品匯入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null);
ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElseGet(() -> getDefaultTemplate());
System.out.println(secondImportTemplate);
}
public static ImportTemplate getDefaultTemplate() {
System.out.println("getDefaultTemplate");
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
輸出結果:
從輸出結果看,2個方法好像差不多,第1次呼叫都返回了預設模版,第2次呼叫都返回了傳入的模版,但其實仔細觀察,你會發現當使用
orElse方法時,getDefaultTemplate方法執行了2次,但呼叫orElseGet方法時,getDefaultTemplate方法只執行了2次(只在第1次傳入模版為null時執行了)。
為什麼會這樣呢?帶著這個疑問,我們看下這2個方法的原始碼,其中orElse方法的原始碼如下所示:
public T orElse(T other) {
return value != null ? value : other;
}
可以看到,引數other是個物件,這個引數肯定是要傳的,但只有value為空時,才會用到(返回)這個物件。
orElseGet方法的原始碼如下所示:
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
可以看到,引數other並不是直接傳入物件,如果value為null,才會執行傳入的引數獲取物件,如果不為null,直接返回value。
2.7 自定義異常
Optional類提供了orElseThrow方法,用於當傳入的物件為null時,丟擲自定義的異常,使用方法如下所示:
public static void main(String[] args) {
ImportTemplate importTemplate = new ImportTemplate(2, "銷售訂單-不定規格商品匯入模版", "o_w-a7109db89f8d4508b4c6202889a1a2c1", null);
ImportTemplate firstImportTemplate = Optional.ofNullable(importTemplate)
.orElseThrow(() -> new IndexOutOfBoundsException());
System.out.println(firstImportTemplate);
importTemplate = null;
ImportTemplate secondImportTemplate = Optional.ofNullable(importTemplate)
.orElseThrow(() -> new IndexOutOfBoundsException());
System.out.println(secondImportTemplate);
}
輸出結果:
2.8 過濾資料
Optional類提供了filter方法來過濾資料,該方法接收一個Predicate引數,返回匹配條件的資料,如果不匹配條件,返回一個空的Optional,使用方法如下所示:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<ImportTemplate> filterById = Optional.ofNullable(importTemplate)
.filter(f -> f.getTemplateId() == 1);
System.out.println(filterById.isPresent());
Optional<ImportTemplate> filterByName = Optional.ofNullable(importTemplate)
.filter(f -> f.getTemplateName().contains("發貨單"));
System.out.println(filterByName.isPresent());
}
public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
輸出結果:
2.9 轉換值
Optional類提供了以下2個方法來轉換值:
- map
- flatMap
map方法的使用方法如下所示:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<String> optionalUrl = Optional.ofNullable(importTemplate)
.map(f -> "url:" + f.getUrl());
System.out.println(optionalUrl.isPresent());
System.out.println(optionalUrl.get());
}
public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
輸出結果:
flatMap方法和map方法類似,不過它支援傳入Optional,使用方法如下所示:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
Optional<String> optionalUrl = Optional.ofNullable(importTemplate)
.flatMap(f -> Optional.ofNullable(f.getUrl()));
System.out.println(optionalUrl.isPresent());
System.out.println(optionalUrl.get());
}
public static ImportTemplate getImportTemplateById(int id) {
return new ImportTemplate(1, "銷售訂單-普通商品匯入模版", "o_w-140e3c1f41c94f238196539558e25bf7", null);
}
輸出結果:
3. 總結
對於程式設計師來說,一不注意就會出現NullPointerException
異常,避免它的方式也很簡單,比如使用前判斷不能為空:
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate != null) {
System.out.println(importTemplate.getUrl());
}
}
比如為空時,直接返回(或者返回預設值):
public static void main(String[] args) {
ImportTemplate importTemplate = getImportTemplateById(1);
if (importTemplate == null) {
return;
}
System.out.println(importTemplate.getUrl());
}
比如,使用本文中的Optional。
使用哪種方式不重要,儘可能地避免NullPointerException
異常才重要。