Java 中的 Objects.requireNonNull() 指南
一、簡介
NullPointerException
是 Java 最常見的例外之一。當存取指向null
引用變數或與之互動時,會發生這種情況。驗證對象,特別是當它們是方法或建構函數中的參數時,對於確保程式碼的健全和可靠非常重要。我們可以透過編寫空檢查程式碼、使用第三方函式庫或選擇更方便的方法來做到這一點。
在本教程中,我們將研究後者—Java 透過Objects.requireNonNull()
方法提供的靈活的內建解決方案。
2. 空值處理
簡單回顧一下,有許多替代方法可以避免手動null
檢查。我們可以從各種函式庫中進行選擇,而不是用if
語句包裝我們的程式碼,這可能容易出錯且耗時。 Spring、Lombok ( @NonNull
) 和 Uber 的 NullAway 只是其中的幾個。
相反,如果我們想保持一致性,避免在程式碼庫中引入額外的庫,或使用普通 Java,我們可以在Optional
類別或Objects
方法之間進行選擇。雖然Optional
簡化了null
值處理,但它通常會增加編碼開銷並使簡單用例的程式碼變得混亂。由於它是一個單獨的對象,因此它也會消耗額外的記憶體。此外, Optional
僅將NullPointerException
轉換為NoSuchElementException
,即它並不能解決缺陷。
另一方面, Objects
類別提供靜態實用方法來更有效地處理物件。它在 Java 7 中引入,並在 Java 8 和 9 中多次更新,它還提供了在執行操作之前驗證條件的方法。
Objects.requireNonNull()
的優點
Objects
類別透過提供一組requireNonNull()
靜態方法來簡化檢查和處理null
值。這些方法提供了一種簡單明了的方法來在使用前檢查null
物件。
requireNonNull()
方法的主要目的是檢查物件參考是否為null
,如果是則拋出NullPointerException
。透過明確拋出異常,我們傳達了檢查的意圖,使程式碼更易於理解和維護。這也告知開發人員和維護人員該錯誤是故意的,這有助於防止隨著時間的推移無意中改變行為。
Objects
類別是java.util
套件的一部分,這使得它可以輕鬆訪問,而不需要外部程式庫或依賴項。其方法有詳細記錄,為開發人員提供了正確使用的明確說明。
4. 用例和重載方法
現在讓我們專注於requireNonNull()
方法的各種用例和重載變體。
這些方法允許在不同的場景中處理null
值,從簡單的null
檢查到使用自訂訊息的更複雜的驗證。
此外,另外兩個方法支援預設值 - requireNonNullElse()
和requireNonNullElseGet()
。透過理解這些用例,我們可以有效地將requireNonNull()
合併到我們的程式碼庫中。
4.1.方法和建構函數中的單參數驗證
首先,讓我們來看看最簡單的實作-帶有單一參數的requireNonNull()
:
public static <T> T requireNonNull(T obj) {
if (obj == null) {
throw new NullPointerException();
} else {
return obj;
}
}
此方法檢查提供的物件參考是否為null
。如果是,它會拋出NullPointerException
。否則,傳回未更改的物件。
此方法主要用於方法和建構函數中的參數驗證。例如,為了確保傳遞給greet()
方法的name
不為null
,我們可以使用下列方法:
void greet(String name) {
Objects.requireNonNull(name, "Name cannot be null");
logger.info("Hello, {}!", name);
}
@Test
void givenObject_whenGreet_thenNoException() {
Assertions.assertDoesNotThrow(() -> greet("Baeldung"));
}
@Test
void givenNull_whenGreet_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> greet(null));
}
透過使用requireNonNull()
,我們可以確保方法接收有效的參數。我們可以在使用null
物件之前檢測它們,從而防止在存取這些物件時發生錯誤。
4.2.使用自訂錯誤訊息進行多參數驗證
接下來,讓我們檢查如何處理方法或建構函式接收多個參數的情況。 Objects.requireNonNull()
的一個變體接受檢查是否為空的物件參考旁邊的String
訊息。如果物件引用為null
則可以拋出帶有自訂錯誤訊息的NullPointerException
:
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
我們可以使用此方法在具有多個參數的建構函式中進行驗證:
static class User {
private final String username;
private final String password;
public User(String username, String password) {
this.username = Objects.requireNonNull(username, "Username is null!");
this.password = Objects.requireNonNull(password, "Password is null!");
}
// getters
}
@Test
void givenValidInput_whenNewUser_thenNoException() {
Assertions.assertDoesNotThrow(() -> new User("Baeldung", "Secret"));
}
@Test
void givenNull_whenNewUser_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> new User(null, "Secret"));
Assertions.assertThrows(NullPointerException.class, () -> new User("Baeldung", null));
}
帶有自訂訊息的NullPointerException
透過指定出現的問題以及導致問題的參數來提供更多上下文。這反過來又改進了錯誤報告並使調試更容易。
4.3.推遲與Supplier
的消息創建
最後,我們可以使用另一個重載方法自訂NullPointerException
:
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null) {
throw new NullPointerException(messageSupplier == null ? null : (String)messageSupplier.get());
} else {
return obj;
}
}
第二個參數是錯誤訊息的Supplier
。它允許將訊息創建推遲到null
檢查之後,這可以提高效能。這在昂貴的操作中特別有價值,例如字串連接或複雜的計算:
void processOrder(UUID orderId) {
Objects.requireNonNull(orderId, () -> {
String message = "Order ID cannot be null! Current timestamp: " + getProcessTimestamp();
message = message.concat("Total number of invalid orders: " + getOrderAmount());
message = message.concat("Please provide a valid order.");
return message;
});
logger.info("Processing order with id: {}", orderId);
}
private static int getOrderAmount() {
return new Random().nextInt(100_000);
}
private static Instant getProcessTimestamp() {
return Instant.now();
}
@Test
void givenObject_whenProcessOrder_thenNoException() {
Assertions.assertDoesNotThrow(() -> processOrder(UUID.randomUUID()));
}
@Test
void givenNull_whenProcessOrder_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> processOrder(null));
}
在上面的範例中, getOrderAmount()
和getProcessTimestamp()
可能涉及耗時的操作,例如資料庫查詢或外部 API 呼叫。透過延遲訊息創建,此方法可以防止orderId
不為null
時不必要的效能成本。
儘管如此,重要的是要確保建立訊息Supplier
的開銷低於直接產生String
訊息的開銷。
5. 最佳實踐
正如我們之前所看到的,在設計方法和建構函數時,我們需要強制執行參數限制以確保程式碼的可預測行為。在方法或建構子的開頭使用Objects.requireNonNull()
有助於及早捕獲無效參數。這種做法還可以使我們的程式碼保持乾淨,更易於維護,並且更易於調試。
更重要的是, requireNonNull()
對於建置快速失敗系統至關重要。快速失敗原則意味著立即偵測到錯誤,防止級聯故障並降低偵錯複雜性。如果一個方法預先驗證其參數,它會很快失敗並出現明顯的異常,從而使問題的根源顯而易見。這些檢查對於系統來說是必要的,以避免令人困惑的錯誤、不正確的結果,甚至程式碼的不相關部分出現問題。
另一個好的做法是明確記錄公共或受保護方法的參數限制。我們可以使用 Javadoc @throws
標籤來指定違反參數限制時拋出的例外。如果多個方法拋出NullPointerException
,我們可以在類別級 Javadoc 中對此進行介紹,而不是對每個方法重複它。
六、結論
在本教程中,我們示範如何使用Objects.requireNonNull()
及其重載變體有效地驗證方法和建構子參數。這些方法提供了一種簡單而強大的方法來處理 Java 中的null
檢查。對自訂錯誤訊息和延遲訊息建立的內建支援增加了靈活性,使其適合各種用例。
此外,採用最佳實踐(例如強制參數限制、使用快速失敗原則和記錄異常)可以提高程式碼庫的整體品質和可維護性。
與往常一樣,完整的原始程式碼可以在 GitHub 上取得。