使用 JSpecify 實現 Java 中空安全的實用指南
1. 簡介
讓 Java 開發者感到沮喪的常見原因之一是NullPointerException.
無論是在大型程式碼庫中工作,還是進行 API 調用,Java 開發者總是必須問自己:「如果返回 null 怎麼辦?」以及如何處理它。儘管 Java 是一種靜態類型語言,但它對 null 值的處理始終存在歧義。
最近,Java 社群已採取措施解決這個問題。 JSpecify 是該領域最有前景的開發之一。
在本教程中,讓我們探索什麼是 JSpecify 以及如何在我們的專案中實現它。
2.什麼是 JSpecify?
Jspecify 提供了一組標準註解,用於明確聲明 Java 程式碼的空值期望。 Jspecify與工具無關,這意味著它不依賴任何特定的框架或 IDE。它適用於整個 Java 生態系統。
它允許開發人員註解方法、欄位或參數(包括泛型參數)是否可以保存空值。這有助於 IDE、靜態分析工具和編譯器在開發過程中捕獲潛在的與空值相關的問題。
雖然過去存在用於空值檢查的註解,但問題在於不同的項目和工具經常使用不同的註解,其意義略有不同。然而,JSpecify 致力於將這些工作統一在一個精確、一致且可互通的標準之下。
3. 為什麼要關心空安全?
從歷史上看,Java 依賴隱式可空性,其中變數根據預設行為或上下文被假定為可空或不可空,而無需開發人員每次都明確指定它。
如果工具能夠辨識零期望,它們就能在我們違反零期望時發出警告。這有助於我們在開發早期發現錯誤。
此外,當我們明確指定可空性時,API 的使用者可以立即了解某個方法是否可以傳回 null,或者某個參數是否可以接受 null。他們的 IDE 會以提示或警告的形式顯示可空性資訊。
4. 如何使用 JSpecify
JSpecify 允許我們指定各種註解來表達空值。
要開始使用 Jspecify,我們需要新增以下相依性:
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
JSpecify 提供各種註解來表達空值。
@Nullable
註解表示被註解的元素可以合法地為空。 @NonNull
表示被註解的元素絕對不能為空。
我們也可以在套件或類別層級指定是否為空。例如, @NullMarked
註解應用於套件、類別或模組,表示預設情況下,所有未註解的類型都被視為非空。
類似地,我們有@NullUnmarked
註釋,它取消了@NullMarked
的效果並允許未註釋的類型具有未指定的空值。
這些註解提供了靈活性。例如,我們可以使用@NullMarked
預設將所有內容設為非空,並僅在可接受 null 的位置使用@Nullable
明確註解。這樣,我們可以減少所需的註解數量。
一旦我們新增了依賴項,我們就可以開始在程式碼中使用註釋,如下所示:
@Nullable
private String findNicknameOrNull(String userId) {
if ("user123".equals(userId)) {
return "CoolUser";
} else {
return null;
}
}
@Test
void givenUnknownUserId_whenFindNicknameOrNull_thenReturnsNull() {
String nickname = findNicknameOrNull("unknownUser");
assertNull(nickname);
}
@Test
void givenNullableMethodResult_whenWrappedInOptional_thenHandledSafely() {
String nickname = findNicknameOrNull("unknownUser");
Optional<String> safeNickname = Optional.ofNullable(nickname);
assertTrue(safeNickname.isEmpty());
}
在上面的程式碼中,方法findNicknameOrNull(String userId)
帶有@Nullable
註解,這會向開發人員發出訊號,表示它可能會傳回null
。這有助於在編譯時捕獲與 null 相關的潛在問題。但是,由於 JSpecify 在運行時不起作用,因此此處的測試驗證了預期的運行時行為:當未找到用戶時,該方法確實會傳回 null 。
不過,在第二個測試中,我們使用Optional
來安全地封裝空值,從而消除了NullPointerException
的風險。
5. 與其他空檢查方法的比較
在 JSpecify 之前,我們還有其他方法來檢查可空性。在本節中,我們將探索編寫null-
安全程式碼的各種方法。
5.1. 使用Optional
java.util.Optional
是容器對象,它包裝返回值,返回值要麼包含非空值,要麼表示不存在。它提供了一種類型安全且顯式的方式來處理值可能不存在的情況,從而降低了拋出NullPointerException
的風險。 Optional Optional
方法的呼叫者有意識地處理這兩種情況。
例如,我們來看看下面的程式碼:
private Optional<String> findNickname(String userId) {
if ("user123".equals(userId)) {
return Optional.of("CoolUser");
} else {
return Optional.empty();
}
}
該方法永遠不會傳回null
。它始終傳回一個Optional
。當有Optional.of(value)
。當沒有值時, Optional.empty()
。
現在,讓我們來看看從呼叫方法上我們是如何處理Optional
:
@Test
void givenKnownUserId_whenFindNickname_thenReturnsOptionalWithValue() {
Optional<String> nickname = findNickname("user123");
assertTrue(nickname.isPresent());
assertEquals("CoolUser", nickname.get());
}
@Test
void givenUnknownUserId_whenFindNickname_thenReturnsEmptyOptional() {
Optional<String> nickname = findNickname("unknownUser");
assertTrue(nickname.isEmpty());
}
從上面的程式碼我們可以看出,呼叫者要麼接收帶有值的Optional
,要麼接收空的Optional
,並且必須明確處理存在與否,這樣就降低了出現NullPointerException
的風險。
然而, Optional
主要用於方法傳回類型,而不是用於欄位或方法參數。將Optional
用於欄位或方法參數會帶來額外的複雜性。
此外,由於物件的創建和包裝, Optional
引入了少量的性能成本。
5.2. 使用Objects.requireNonNull()
處理潛在 null 相關問題的另一種常見做法是使用執行時間斷言,即Objects.requireNonNull
。如果提供的參數為 null,此方法會立即拋出NullPointerException
。
例如,我們來看看這段程式碼:
@Test
void givenNonNullArgument_whenValidate_thenDoesNotThrowException() {
String result = processNickname("CoolUser");
assertEquals("Processed: CoolUser", result);
}
@Test
void givenNullArgument_whenValidate_thenThrowsNullPointerException() {
assertThrows(NullPointerException.class, () -> processNickname(null));
}
private String processNickname(String nickname) {
Objects.requireNonNull(nickname, "Nickname must not be null");
return "Processed: " + nickname;
}
從上面的程式碼可以看出,如果參數為null
,則會立即拋出NullPointerException
。這使得 bug 在測試過程中更容易被發現,因為它們會在早期就失敗,而不是悄無聲息地流入更深層的邏輯中。
透過在方法開始時驗證輸入,我們可以在開發或測試期間發現違規行為;這將防止生產期間出現錯誤。
然而, requireNonNull()
的一個關鍵限制是它只能在運行時檢測問題。它們不提供像 JSpecify 那樣的編譯時或 IDE 提示。
6. 明確採用策略
一次性註釋整個程式碼庫是不切實際的。幸好,JSpecify 允許逐步採用。我們可以在不破壞程式碼的情況下逐步實現null
安全註解。
一個典型的採用策略是,先為小型、獨立的套件或類別添加註解,然後使用@NullMarked
強制使用非空預設值,以減少註解噪音。之後,我們可以在必要時明確添加@Nullable
註解。
我們還可以運行靜態分析工具來捕獲任何不匹配並改進我們的註釋,然後逐步將覆蓋範圍擴展到程式碼庫的更廣泛部分。
7. 工具和生態系統支持
現在許多流行的工具和 IDE 都不同程度地支援 JSpecify 註解。
例如,流行的靜態分析工具 Checker Framework 有自己的空安全註解;然而,它在最近的版本中開始支援 JSpecify 的核心註解。
同樣,NullAway 是另一個專注於偵測可空性問題的靜態分析工具。它現在支援 JSpecify 註解。
從 IDE 的前端來看,IntelliJ IDEA 長期以來一直支援空值註解,包括其自身的註解。 IntelliJ 現在提供了 JSpecify 註解的基本識別,可突顯不匹配的情況和潛在的可空性問題。
8. 結論
在本文中,我們討論了 JSpecify 工具,它能夠在很大程度上幫助開發人員擺脫與 null 相關的錯誤。它無疑使 Java 程式碼庫更加健壯、更具彈性,並減少了生產過程中的意外情況。雖然工具支援仍在不斷完善,但 JSpecify 的發展勢頭表明,它很快就會成為 Java 中表示 null 的預設方法。
與往常一樣,程式碼範例可在 GitHub 上找到。