Java中空指針異常的幾種解決方案
Java 中任何對(duì)象都有可能為空,當(dāng)我們調(diào)用空對(duì)象的方法時(shí)就會(huì)拋出 NullPointerException 空指針異常,這是一種非常常見(jiàn)的錯(cuò)誤類(lèi)型。
我們可以使用若干種方法來(lái)避免產(chǎn)生這類(lèi)異常,使得我們的代碼更為健壯。
本文將列舉這些解決方案,包括傳統(tǒng)的空值檢測(cè)、編程規(guī)范、以及使用現(xiàn)代 Java 語(yǔ)言引入的各類(lèi)工具來(lái)作為輔助。
運(yùn)行時(shí)檢測(cè)
最顯而易見(jiàn)的方法就是使用 if (obj == null) 來(lái)對(duì)所有需要用到的對(duì)象來(lái)進(jìn)行檢測(cè),包括函數(shù)參數(shù)、返回值、以及類(lèi)實(shí)例的成員變量。
當(dāng)你檢測(cè)到 null 值時(shí),可以選擇拋出更具針對(duì)性的異常類(lèi)型,如 IllegalArgumentException,并添加消息內(nèi)容。
我們可以使用一些庫(kù)函數(shù)來(lái)簡(jiǎn)化代碼,如 Java 7 開(kāi)始提供的 Objects#requireNonNull 方法:
public void testObjects(Object arg) {
Object checked = Objects.requireNonNull(arg, "arg must not be null");
checked.toString();
}
Guava 的 Preconditions 類(lèi)中也提供了一系列用于檢測(cè)參數(shù)合法性的工具函數(shù),其中就包含空值檢測(cè):
public void testGuava(Object arg) {
Object checked = Preconditions.checkNotNull(arg, "%s must not be null", "arg");
checked.toString();
}
我們還可以使用 Lombok 來(lái)生成空值檢測(cè)代碼,并拋出帶有提示信息的空指針異常:
public void testLombok(@NonNull Object arg) {
arg.toString();
}
生成的代碼如下:
public void testLombokGenerated(Object arg) {
if (arg == null) {
throw new NullPointerException("arg is marked @NonNull but is null");
}
arg.toString();
}
這個(gè)注解還可以用在類(lèi)實(shí)例的成員變量上,所有的賦值操作會(huì)自動(dòng)進(jìn)行空值檢測(cè)。
編程規(guī)范
通過(guò)遵守某些編程規(guī)范,也可以從一定程度上減少空指針異常的發(fā)生。
使用那些已經(jīng)對(duì) null 值做過(guò)判斷的方法,如 String#equals、String#valueOf、以及三方庫(kù)中用來(lái)判斷字符串和集合是否為空的函數(shù):
if (str != null && str.equals("text")) {}
if ("text".equals(str)) {}
if (obj != null) { obj.toString(); }
String.valueOf(obj); // "null"
// from spring-core
StringUtils.isEmpty(str);
CollectionUtils.isEmpty(col);
// from guava
Strings.isNullOrEmpty(str);
// from commons-collections4
CollectionUtils.isEmpty(col);
如果函數(shù)的某個(gè)參數(shù)可以接收 null 值,考慮改寫(xiě)成兩個(gè)函數(shù),使用不同的函數(shù)簽名,這樣就可以強(qiáng)制要求每個(gè)參數(shù)都不為空了:
public void methodA(Object arg1) {
methodB(arg1, new Object[0]);
}
public void methodB(Object arg1, Object[] arg2) {
for (Object obj : arg2) {} // no null check
}
如果函數(shù)的返回值是集合類(lèi)型,當(dāng)結(jié)果為空時(shí),不要返回 null 值,而是返回一個(gè)空的集合;如果返回值類(lèi)型是對(duì)象,則可以選擇拋出異常。
Spring JdbcTemplate 正是使用了這種處理方式:
// 當(dāng)查詢(xún)結(jié)果為空時(shí),返回 new ArrayList<>()
jdbcTemplate.queryForList("SELECT * FROM person");
// 若找不到該條記錄,則拋出 EmptyResultDataAccessException
jdbcTemplate.queryForObject("SELECT age FROM person WHERE id = 1", Integer.class);
// 支持泛型集合
public <T> List<T> testReturnCollection() {
return Collections.emptyList();
}
靜態(tài)代碼分析
Java 語(yǔ)言有許多靜態(tài)代碼分析工具,如 Eclipse IDE、SpotBugs、Checker Framework 等,它們可以幫助程序員檢測(cè)出編譯期的錯(cuò)誤。
結(jié)合 @Nullable 和 @Nonnull 等注解,我們就可以在程序運(yùn)行之前發(fā)現(xiàn)可能拋出空指針異常的代碼。
但是,空值檢測(cè)注解還沒(méi)有得到標(biāo)準(zhǔn)化。
雖然 2006 年 9 月社區(qū)提出了 JSR 305 規(guī)范,但它長(zhǎng)期處于擱置狀態(tài)。
很多第三方庫(kù)提供了類(lèi)似的注解,且得到了不同工具的支持,其中使用較多的有:
javax.annotation.Nonnull:由 JSR 305 提出,其參考實(shí)現(xiàn)為com.google.code.findbugs.jsr305;org.eclipse.jdt.annotation.NonNull:Eclipse IDE 原生支持的空值檢測(cè)注解;edu.umd.cs.findbugs.annotations.NonNull:SpotBugs 使用的注解,基于findbugs.jsr305;org.springframework.lang.NonNull:Spring Framework 5.0 開(kāi)始提供;org.checkerframework.checker.nullness.qual.NonNull:Checker Framework 使用;android.support.annotation.NonNull:集成在安卓開(kāi)發(fā)工具中;
我建議使用一種跨 IDE 的解決方案,如 SpotBugs 或 Checker Framework,它們都能和 Maven 結(jié)合得很好。
SpotBugs 與 @NonNull、@CheckForNull
SpotBugs 是 FindBugs 的后繼者。通過(guò)在方法的參數(shù)和返回值上添加 @NonNull 和 @CheckForNull 注解,SpotBugs 可以幫助我們進(jìn)行編譯期的空值檢測(cè)。
需要注意的是,SpotBugs 不支持 @Nullable 注解,必須用 @CheckForNull 代替。
如官方文檔中所說(shuō),僅當(dāng)需要覆蓋 @ParametersAreNonnullByDefault 時(shí)才會(huì)用到 @Nullable。
官方文檔 中說(shuō)明了如何將 SpotBugs 應(yīng)用到 Maven 和 Eclipse 中去。我們還需要將 spotbugs-annotations 加入到項(xiàng)目依賴(lài)中,以便使用對(duì)應(yīng)的注解。
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>3.1.7</version>
</dependency>
以下是對(duì)不同使用場(chǎng)景的說(shuō)明:
@NonNull
private Object returnNonNull() {
// 錯(cuò)誤:returnNonNull() 可能返回空值,但其已聲明為 @Nonnull
return null;
}
@CheckForNull
private Object returnNullable() {
return null;
}
public void testReturnNullable() {
Object obj = returnNullable();
// 錯(cuò)誤:方法的返回值可能為空
System.out.println(obj.toString());
}
private void argumentNonNull(@NonNull Object arg) {
System.out.println(arg.toString());
}
public void testArgumentNonNull() {
// 錯(cuò)誤:不能將 null 傳遞給非空參數(shù)
argumentNonNull(null);
}
public void testNullableArgument(@CheckForNull Object arg) {
// 錯(cuò)誤:參數(shù)可能為空
System.out.println(arg.toString());
}
對(duì)于 Eclipse 用戶(hù),還可以使用 IDE 內(nèi)置的空值檢測(cè)工具,只需將默認(rèn)的注解 org.eclipse.jdt.annotation.Nullable 替換為 SpotBugs 的注解即可:

Checker Framework 與 @NonNull、@Nullable
Checker Framework 能夠作為 javac 編譯器的插件運(yùn)行,對(duì)代碼中的數(shù)據(jù)類(lèi)型進(jìn)行檢測(cè),預(yù)防各類(lèi)問(wèn)題。
我們可以參照 官方文檔,將 Checker Framework 與 maven-compiler-plugin 結(jié)合,之后每次執(zhí)行 mvn compile 時(shí)就會(huì)進(jìn)行檢查。
Checker Framework 的空值檢測(cè)程序支持幾乎所有的注解,包括 JSR 305、Eclipse、甚至 lombok.NonNull。
import org.checkerframework.checker.nullness.qual.Nullable;
@Nullable
private Object returnNullable() {
return null;
}
public void testReturnNullable() {
Object obj = returnNullable();
// 錯(cuò)誤:obj 可能為空
System.out.println(obj.toString());
}
Checker Framework 默認(rèn)會(huì)將 @NonNull 應(yīng)用到所有的函數(shù)參數(shù)和返回值上,因此,即使不添加這個(gè)注解,以下程序也是無(wú)法編譯通過(guò)的:
private Object returnNonNull() {
// 錯(cuò)誤:方法聲明為 @NonNull,但返回的是 null。
return null;
}
private void argumentNonNull(Object arg) {
System.out.println(arg.toString());
}
public void testArgumentNonNull() {
// 錯(cuò)誤:參數(shù)聲明為 @NonNull,但傳入的是 null。
argumentNonNull(null);
}
Checker Framework 對(duì)使用 Spring Framework 5.0 以上的用戶(hù)非常有用,因?yàn)?Spring 提供了內(nèi)置的空值檢測(cè)注解,且能夠被 Checker Framework 支持。
一方面我們無(wú)需再引入額外的 Jar 包,更重要的是 Spring Framework 代碼本身就使用了這些注解,這樣我們?cè)谡{(diào)用它的 API 時(shí)就能有效地處理空值了。
舉例來(lái)說(shuō),StringUtils 類(lèi)里可以傳入空值的函數(shù)、以及會(huì)返回空值的函數(shù)都添加了 @Nullable 注解,而未添加的方法則繼承了整個(gè)框架的 @NonNull 注解,因此,下列代碼中的空指針異常就可以被 Checker Framework 檢測(cè)到了:
// 這是 spring-core 中定義的類(lèi)和方法
public abstract class StringUtils {
// str 參數(shù)繼承了全局的 @NonNull 注解
public static String capitalize(String str) {}
@Nullable
public static String getFilename(@Nullable String path) {}
}
// 錯(cuò)誤:參數(shù)聲明為 @NonNull,但傳入的是 null。
StringUtils.capitalize(null);
String filename = StringUtils.getFilename("/path/to/file");
// 錯(cuò)誤:filename 可能為空。
System.out.println(filename.length());
Optional 類(lèi)型
Java 8 引入了 Optional<T> 類(lèi)型,我們可以用它來(lái)對(duì)函數(shù)的返回值進(jìn)行包裝。
這種方式的優(yōu)點(diǎn)是可以明確定義該方法是有可能返回空值的,因此調(diào)用方必須做好相應(yīng)處理,這樣也就不會(huì)引發(fā)空指針異常。
但是,也不可避免地需要編寫(xiě)更多代碼,而且會(huì)產(chǎn)生很多垃圾對(duì)象,增加 GC 的壓力,因此在使用時(shí)需要酌情考慮。
Optional<String> opt;
// 創(chuàng)建
opt = Optional.empty();
opt = Optional.of("text");
opt = Optional.ofNullable(null);
// 判斷并讀取
if (opt.isPresent()) {
opt.get();
}
// 默認(rèn)值
opt.orElse("default");
opt.orElseGet(() -> "default");
opt.orElseThrow(() -> new NullPointerException());
// 相關(guān)操作
opt.ifPresent(value -> {
System.out.println(value);
});
opt.filter(value -> value.length() > 5);
opt.map(value -> value.trim());
opt.flatMap(value -> {
String trimmed = value.trim();
return trimmed.isEmpty() ? Optional.empty() : Optional.of(trimmed);
});
方法的鏈?zhǔn)秸{(diào)用很容易引發(fā)空指針異常,但如果返回值都用 Optional 包裝起來(lái),就可以用 flatMap 方法來(lái)實(shí)現(xiàn)安全的鏈?zhǔn)秸{(diào)用了:
String zipCode = getUser()
.flatMap(User::getAddress)
.flatMap(Address::getZipCode)
.orElse("");
Java 8 Stream API 同樣使用了 Optional 作為返回類(lèi)型:
stringList.stream().findFirst().orElse("default");
stringList.stream()
.max(Comparator.naturalOrder())
.ifPresent(System.out::println);
此外,Java 8 還針對(duì)基礎(chǔ)類(lèi)型提供了單獨(dú)的 Optional 類(lèi),如 OptionalInt、OptionalDouble 等,在性能要求比較高的場(chǎng)景下很適用。
其它 JVM 語(yǔ)言中的空指針異常
Scala 語(yǔ)言中的 Option 類(lèi)可以對(duì)標(biāo) Java 8 的 Optional。
它有兩個(gè)子類(lèi)型,Some 表示有值,None 表示空。
val opt: Option[String] = Some("text")
opt.getOrElse("default")
除了使用 Option#isEmpty 判斷,還可以使用 Scala 的模式匹配:
opt match {
case Some(text) => println(text)
case None => println("default")
}
Scala 的集合處理函數(shù)庫(kù)非常強(qiáng)大,Option 則可直接作為集合進(jìn)行操作,如 filer、map、以及列表解析(for-comprehension):
opt.map(_.trim).filter(_.length > 0).map(_.toUpperCase).getOrElse("DEFAULT")
val upper = for {
text <- opt
trimmed <- Some(text.trim())
upper <- Some(trimmed) if trimmed.length > 0
} yield upper
upper.getOrElse("DEFAULT")
Kotlin 使用了另一種方式,用戶(hù)在定義變量時(shí)就需要明確區(qū)分 可空和不可空類(lèi)型。當(dāng)可空類(lèi)型被使用時(shí),就必須進(jìn)行空值檢測(cè)。
var a: String = "text" a = null // 錯(cuò)誤:無(wú)法將 null 賦值給非空 String 類(lèi)型。 val b: String? = "text" // 錯(cuò)誤:操作可空類(lèi)型時(shí)必須使用安全操作符(?.)或強(qiáng)制忽略(!!.)。 println(b.length) val l: Int? = b?.length // 安全操作 b!!.length // 強(qiáng)制忽略,可能引發(fā)空值異常
Kotlin 的特性之一是與 Java 的可互操作性,但 Kotlin 編譯器無(wú)法知曉 Java 類(lèi)型是否為空,這就需要在 Java 代碼中使用注解了,而 Kotlin 支持的 注解 也非常廣泛。
Spring Framework 5.0 起原生支持 Kotlin,其空值檢測(cè)也是通過(guò)注解進(jìn)行的,使得 Kotlin 可以安全地調(diào)用 Spring Framework 的所有 API。
結(jié)論
在以上這些方案中,我比較推薦使用注解來(lái)預(yù)防空指針異常,因?yàn)檫@種方式十分有效,對(duì)代碼的侵入性也較小。
所有的公共 API 都應(yīng)該使用 @Nullable 和 @NonNull 進(jìn)行注解,這樣就能強(qiáng)制調(diào)用方對(duì)空指針異常進(jìn)行預(yù)防,讓我們的程序更為健壯。希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
參考資料
http://jmri.sourceforge.net/help/en/html/doc/Technical/SpotBugs.shtml
https://dzone.com/articles/features-to-avoid-null-reference-exceptions-java-a
相關(guān)文章
SpringCloud中的OpenFeign調(diào)用解讀
OpenFeign是一個(gè)顯示聲明式的WebService客戶(hù)端,使用OpenFeign能讓編寫(xiě)Web Service客戶(hù)端更加簡(jiǎn)單OpenFeign的設(shè)計(jì)宗旨式簡(jiǎn)化Java Http客戶(hù)端的開(kāi)發(fā),本文給大家介紹SpringCloud之OpenFeign調(diào)用解讀,感興趣的朋友一起看看吧2023-11-11
Spring Boot中的那些條件判斷的實(shí)現(xiàn)方法
這篇文章主要介紹了Spring Boot中的那些條件判斷的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Java設(shè)計(jì)模式之備忘錄模式(Memento模式)介紹
這篇文章主要介紹了Java設(shè)計(jì)模式之備忘錄模式(Memento模式)介紹,memento是一個(gè)保存另外一個(gè)對(duì)象內(nèi)部狀態(tài)拷貝的對(duì)象,這樣以后就可以將該對(duì)象恢復(fù)到原先保存的狀態(tài),需要的朋友可以參考下2015-03-03
Java設(shè)計(jì)模式之中介模式(Mediator模式)介紹
這篇文章主要介紹了Java設(shè)計(jì)模式之中介模式(Mediator模式)介紹,本文講解了為何使用Mediator模式、如何使用中介模式等內(nèi)容,需要的朋友可以參考下2015-03-03

