Java 斷言 Assert 使用教程與最佳實踐

程序猿阿朗發表於2024-04-24

本文收錄於 Github.com/niumoo/JavaNotes,Java 系列文件,資料結構與演算法!
本文收錄於網站:https://www.wdbyte.com/,我的公眾號:程式猿阿朗

作為一個 Java 開發者,如果要問你 Java 中有哪些關鍵字,你可能會隨口說出一串,如果問你 Java 有哪些最不常使用的關鍵字,或許你還能說出幾個。但是 assert 關鍵字一定算是其中之一,或者,Java 寫了幾年,還沒有用過 Java 的 assert 關鍵字。

這篇文章介紹 Java assert 的用法、最佳實踐、特殊用法以及替代工具。

Java Assert 簡介

Assert 中文我們一般稱為斷言,你可以理解為 “十分肯定地說” 。很多程式語言中都有斷言,使用斷言可以快速方便的驗證程式中的某個假設條件或者狀態是否成立,不成立則立即丟擲異常。斷言通常用於開發和測試階段。

Java 中的斷言使用 assert 關鍵字實現,但是因為 assert 在 Java 1.4 中才被引入,因此在 Java 1.4 之前,assert 並不是 Java 關鍵字,可能會被寫成普通變數名。新版 Java 嚴格遵守向後相容下,這可能也是 Java 預設禁用斷言的原因之一,開啟斷言可以使用 -ea 引數手動啟用。

java -ea YourClassName

啟用和禁用斷言

基於上述原因,Java 預設關閉了斷言,手動開啟斷言可以使用 -ea 作為 JVM 引數啟動 Java 程式。

-ea-enableassertions 命令的縮寫。

java -ea AssertDemo

也可以使用 -ea:包路徑 只為某些包開啟斷言,如為包 com.wdbyte 中的所有類開啟斷言支援。

-ea:com.wdbyte...

如果某些類庫過於老舊,使用了 assert 作為變數名,為了正常執行, Java 也提供了對某些包禁用斷言的引數。

-da:com.wdbyte...

-da-disableassertions 的縮寫

Java 中使用斷言

Java 中使用斷言有兩種語法。

方式1

assert boolExpression;

使用 assert 關鍵詞緊跟給一個布林條件進行斷言判斷,這種方式斷言失敗時,會丟擲 java.lang.AssertionError 異常,但是沒有具體的錯誤資訊。

舉例:

List<String> list = Arrays.asList("wdbyte", "com");
boolean result = list.remove("x");
assert result;

執行:

Exception in thread "main" java.lang.AssertionError
	at com.wdbyte.assert1.AssertDemo1.main(AssertDemo1.java:14)

方式2

assert boolExpression:msg;

這種方式報錯時會把 msg 透過建構函式賦值給 AssertionError

舉例:

assert result : "移除失敗";

執行:

Exception in thread "main" java.lang.AssertionError: 移除失敗
	at com.wdbyte.assert1.AssertDemo1.main(AssertDemo1.java:15)

Assert 最佳實踐

切記 assert 斷言是一種除錯工具,用於在開發和測試階段檢查程式的某些假設是否為真,它是開發者的一個輔助工具,不應該對線上程式碼的執行產生任何影響。

使用斷言時的最佳實踐是確保它不會成為程式的常規執行流程的一部分,而是作為一種發現內部錯誤和驗證程式假設的手段。在效能敏感或者資源受限的環境中,開應該在開發和測試階段使用斷言,然後在部署生產版本之前禁用它們。

適用場景

  1. 開發和測試階段的臨時檢查

    還是要重複一次這個使用時機,首先因為 assert語句在生產環境下預設是禁用的,其次它可能會對效能產生影響,不應該被用作錯誤處理機制。在開發或除錯期間,當你想要驗證某個假設時,assert可以作為一種快速檢查的方法。這些用法通常在程式碼達到穩定狀態後被移除或替換為更健壯的錯誤處理機制。

  2. 單元測試

    使用斷言對方法的執行結果進行判斷,是單元測試中最為常用的操作。如果斷言不透過,程式會立即丟擲錯誤。良好的程式碼應該編寫對應的單元測試,並且給出儘可能多的測試用例,斷言透過可以保證程式的執行結果在預期之內。

  3. 存在隱含約束條件

    如何理解存在隱含約束條件,比如下面的程式碼示例中,程式碼中 else 部分預設 i%3 的餘數為2,這種可以看做是一個隱含的約束條件。

    if (i % 3 == 0) {
        ...
    } else if (i % 3 == 1) {
        ...
    } else { // 此處,我們認為 (i % 3 == 2)
        ...
    }
    

    在這個例子中,當你本想透過註釋來宣告某個隱含的規則時,可以該改用斷言。因此上述的 if 語句可以這樣改寫:

    if (i % 3 == 0) {
       ...
    } else if (i % 3 == 1) {
        ...
    } else {
        assert i % 3 == 2 : i;
        ...
    }
    

    注意:例子中在 i 為負數時斷言會失敗,這時餘數是負的。

不適用場景

  1. 不要用作引數校驗

    斷言不應該用於引數校驗,首先斷言可能會被禁用,禁用時斷言的語句不會被執行。其次,引數校驗應該丟擲對應的異常,如 NullPointerExceptionIllegalArgumentExceptionIndexOutOfBoundsException.

  2. 不要在斷言中執行程式碼。

    因為斷言可能會被禁用,如果程式碼依賴斷言執行,那麼可能不會被執行。如 assert list.remove("x");; 在斷言禁用時,不會被執行,會造成程式執行結果異常。

    // assert list.remove("x") : "移除失敗"; 不可取,可能不執行
    // 推薦下面的方式
    boolean result = list.remove("x");
    assert result : "移除失敗";
    

Assert 進階用法

編譯階段消除斷言

在效能受限的裝置中開發應用,我們可能會希望完全從類檔案中剔除斷言。雖然可以禁用斷言,但是對於在生產環境中不需要的程式碼,我們還是想盡可能的刪去,這樣不僅減小了類檔案的大小,而且可以在沒有高質量即時編譯器(JIT)的情況下,減少資源佔用並提升執行時效能。

如果你有類似的需求,可以結合 if 在編譯階段消除斷言。

static final boolean asserts = false; // 設定為 false 來消除斷言

public static void main(String[] args) {
    List<String> list = Arrays.asList("wdbyte", "com");
    boolean result = list.remove("x");
    if (asserts) {
        assert result : "移除失敗";
    }
}

因為 if (asserts) 永遠為 false,在編譯階段就會被最佳化,反編譯編譯後的 class 可以發現斷言部分程式碼已經不存在了。

List<String> list = Arrays.asList("wdbyte", "com");
boolean result = list.remove("x");

強制啟用斷言

如果某些關鍵系統希望在指定環境中不能禁用斷言。下面的靜態初始化示例可以實現這個強制條件。

static {
    boolean assertsEnabled = false;
    assert assertsEnabled = true; 
    if (!assertsEnabled) {
        throw new RuntimeException("必須啟用斷言!!!");
    }
}

替代開源庫

在Java中,除了語言內建的assert關鍵字外,許多開源庫都提供了更強大、更靈活的斷言機制,這些工具通常用於單元測試中,但也可以用於生產程式碼中對條件進行驗證。下面列出一些廣泛使用的有斷言功能的開源庫。

  1. JUnit: JUnit是一個廣泛使用的單元測試框架,其中包含用於編寫測試斷言的方法。JUnit 4 使用org.junit.Assert類提供斷言,而JUnit 5 則引入了org.junit.jupiter.api.Assertions類。

    List<String> list = Arrays.asList("wdbyte", "com");
    boolean result = list.remove("x");
    Assertions.assertTrue(result);
    
  2. AssertJ: AssertJ 提供了豐富的、流式的、易於使用的斷言庫,使得錯誤的診斷更為容易。它支援Java 8的特性,比如lambda表示式、Stream和Optional型別的斷言。

    Assertions.assertThat("").isEmpty()
    
  3. Apache Commons Lang : 提供的 Validate 類可以進行常見的條件驗證。

    Validate.isTrue(list.isEmpty(),"msg");
    
  4. Google Guava :Guava 提供了 Preconditions 類可以用於常見的條件驗證,還提供了一個 Verify 類用於斷言操作。

    Preconditions.checkNotNull("","msg");
    Verify.verify(list.isEmpty(),"msg");
    

一如既往,文章中程式碼存放在 Github.com/niumoo/javaNotes.

參考

  1. https://docs.oracle.com/javase/8/docs/technotes/guides/language/assert.html
  2. https://junit.org/
  3. https://github.com/assertj/assertj

本文收錄於 Github.com/niumoo/JavaNotes,Java 系列文件,資料結構與演算法!
本文收錄於網站:https://www.wdbyte.com/,我的公眾號:程式猿阿朗

相關文章