前言
對於程式設計開發者而言空指標異常是非常常見的,基本上各類程式語言都存在空指標異常,對於Java開發者而言,相信NullPointerException是大家再熟悉不過的。雖然空指標很常見,但是空指標對系統造成的危害卻是不容忽視的,因此很多現代程式語言在語法上就對空指標進行了很多避免,比如Kotlin。但是對於Java語言有沒有什麼好的方式呢?
空指標分析
對於空指標的出現,其實一般可以歸納為以下幾個原因:
- 對於方法入參沒有嚴格校驗
- 對於方法返回值沒有嚴格的校驗
但是更本質的原因應該是對於呼叫的其他API沒有充分的瞭解,使用時不知道API方法的入參是否可以接受null,不確定方法的返回值是否為null。
在開發專案的時候相信很多人對於使用的API和方法都會習慣地去看一看方法對應的程式碼看看是否可以接受null,是否可以返回null值,雖然這樣可以有效避免空指標,但是這種方式會降低工作效率,沒法得到大家共識,更沒辦法辦法在部門中進行推廣。所以需要更行之有效的方式。
空指標避免方法
避免空指標的的目標不僅在於有效避免和減少空指標,更在與增加方法可讀性,使呼叫者明確知道方法的入參是否可以接受null,方法的返回值是否可以返回null。
1.返回值空指標避免
在Java8之前相信很多人都使用Google Guava的Optional,能夠有效避免返回值造成的空指標,因此後來也就在Java8中引入了Optional。但是僅僅靠Optional是不夠的。方法的返回型別不是Optional不能說明方法是否會返回null,因此需要一種更明確的方式。而我個人比較推薦的方式就是標註註解,目前很多開源專案也是採用的這種方式。這樣不僅能有效避免空指標,還能增加方法的可讀性。
2.方法入參控制值避免
方法入參是否可以接受null,這裡除了標註註解沒有其他更好的方式。標註註解同樣能提升API的可讀性。
Optional的使用
Optional的選擇和使用比較簡單,在Java8之前使用Guava Optional,在Java8之後使用語言本身自帶的Optional。需要注意的是Optional並不適用於方法入參。
示例:
// Java8
public static Optional<String> valueOf(Integer number) {
if (number == null) {
return Optional.empty();
}
String str = String.valueOf(number);
return Optional.of(number);
}
複製程式碼
註解的使用
對於註解的選擇就比較困難了,因為可選擇的註解太多了,而且需要考慮的因素也比較多。
1.常用的註解:
- findbugs
- edu.umd.cs.findbugs.annotations.NonNull
- edu.umd.cs.findbugs.annotations.Nullable
- jsr305
- javax.annotation.Nonnull
- javax.annotation.Nullable
- spring-core
- org.springframework.lang.NonNull
- org.springframework.lang.Nullable
- javax-validator
- javax.validation.constraints.NotNull
- javax.validation.constraints.Null
- android-support
- android.support.annotation.NonNull
- android.support.annotation.Nullable
- eclipse-jdt
- org.eclipse.jdt.annotation.NonNull
- org.eclipse.jdt.annotation.Nullable
- jetbrains-annotations
- org.jetbrains.annotations.NotNull
- org.jetbrains.annotations.Nullable
- lombok
- lombok.NonNull
- rt.jar
- com.sun.istack.internal.NotNull
- com.sun.istack.internal.Nullable
2.選擇因素:
- 註解完備性
- 必須同時支援null註解與非null註解,如果只支援其中一個那麼使用場景將會受到很大限制
- ide程式碼檢查
- 在ide中執行的時候如果能夠對標註非null的引數和返回值進行校驗,那麼麼將在很大程度上避免空指標
- 校驗邏輯大致如下:
public static void display(@Nonnull String str) { if (str == null) { throw new IllegalArgumentException(); } // do something } 複製程式碼
- ide註解生成
- 繼承父類的方法,是否可以直接繼承標註在方法引數和返回型別上的註解,這個特性是很重要的,因為在大型軟體系統的中都是採用分層架構,層與層之間進行呼叫都是通過介面,因此不支援這個特性將會導致開發人員手動在子類方法入參和返回型別上標註註解,這無疑會大大增加工作量。
- ide智慧提示
- 會對潛在產生空指標的變數進行高亮顯示
- findbugs支援
- 一般的公司都會要求開發人員在ide上安裝findbugs,用以掃描程式碼分析潛在的bug
- sonar支援
- 大型公司都會對程式碼進行靜態分析,一般使用SonarCube,SonarCube可以繼承fingbugs和pmd的校驗規則,因此支援fingdbugs可以在一定程度上說明也支援Sonar
3.各類註解支援情況
註解支援庫 | 空註解 | 非空註解 | findbugs支援 | sonar支援 | ide執行時檢查 | ide智慧提示 | ide程式碼生成 |
---|---|---|---|---|---|---|---|
findbugs | @NonNull | @Nullable | 支援 | 支援 | 支援 | 支援 | 支援 |
jsr305 | @Nonnull | @Nullable | 支援 | 支援 | 支援 | 支援 | 支援 |
spring-core | @NonNull | @Nullable | 不支援 | 不支援 | 不支援 | 支援 | 不支援 |
javax-validator | @NotNull | @Null | 不支援 | 不支援 | 不支援 | 不支援 | 支援 |
android-support | @NonNull | @Nullable | 不支援 | 不支援 | 支援 | 支援 | 支援 |
eclipse-jdt | @NonNull | @Nullable | 不支援 | 不支援 | 不支援 | 支援 | 支援 |
jetbrains-annotations | @NotNull | @Nullable | 不支援 | 不支援 | 支援 | 支援 | 支援 |
lombok | @NonNull | 不支援 | 不支援 | 不支援 | 不支援 | 不支援 | 不支援 |
rt.jar | @NotNull | @Nullable | 不支援 | 不支援 | 不支援 | 不支援 | 不支援 |
注意
- 測試使用的ide是Idea,Eclipse存在一定的差異
- rt.jar @NotNull @Nullable屬於sun的內部包,不要使用,如果有程式碼檢查則不會被允許通過
- eclipse-jdt和jetbrains-annotations和對應的ide繫結比較緊密不要輕易使用
- javax-validator和lombok主要是執行時的引數檢查
- fingbugs原生的註解已經不再推薦,推薦使用jsr305的註解
4.方案選擇
通過以上對比,可以很好的選擇出應該使用的註解是jsr305的@Nullable和@Nonnull。在使用過程中是否意味著所有方法的入參和返回值都應該標註呢?顯然不是的,基本型別的入參和返回值是不需要標註@Nonnull和@Nullable註解的;private方法,package方法,protected方法也是不需要標註的;其實歸納為一句話,就是public方法上的非基本型別引數和返回值需要標註。那麼Optional返回型別是否需要標註呢,這裡沒有一個明確的定論,個人認為覺得標上比較好。
標註註解只是一種宣告宣告,沒有強制的約束,因此標註@NotNull的入參應該增加引數校驗才合理,這也是一般開源專案的普遍做法。
- maven依賴
<dependency> <groupId>com.google.code.findbugs</groupId> <artifactId>jsr305</artifactId> <version>3.0.2</version> </dependency> 複製程式碼
- 使用示例
// 非空註解 @Nonnull public Integer add(@Nonnull Integer number1, @Nonnull Integer number2) { Assert.notNull(number1, "number1 must not be null"); Assert.notNull(number2, "number2 must not be null"); return numnber1 + number2; } // 空註解 public static boolean isBlank(@Nullable String str) { return str == null || str.trim().length() == 0; } // Optional @Nonnull public static Optional<Integer> parseInte(@Nullable String str) { if (str == null) { return Optional.empty(); } return Optional.of(Integer.parseInt(str)); } 複製程式碼
Idea jsr305註解配置
配置之後,idea可以對空指標進行校驗,也能進行智慧提示。
結語
這雖然只是一個很小的改進,但是會極大程度提升程式碼的質量,大幅度降低系統中的空指標異常的數量,提升系統的健壯性。