Java 断言 Assert 使用教程与最佳实践

作为一个 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");
  1. 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