bigywater.tech
Java · 337

assert 断言:开发期不变量如何表达

assert 默认是关的。它在字节码里是什么样?-ea:p... 这样的选项怎么起作用?断言里有副作用会怎样?关闭时真的零成本吗?这篇逐项验证。

0 次
不加 -ea 时 assert 里的 check(-1) 被调用的次数(加 -ea 是 1 次)
0.58 → 10.51 ns
同一个循环里昂贵断言关闭与打开时每次迭代的耗时
初始化时
类的断言状态确定的时机:之后再调用 setClassAssertionStatus 不起作用

字节码

实测 · 本机执行输出
  p.Lib 的字段: ['static final boolean $assertionsDisabled;']
  p.Lib 的 <clinit> 字节码:
    0: ldc #7 // class p/Lib
    2: invokevirtual #9 // Method java/lang/Class.desiredAssertionStatus:()Z
    5: ifne 12
    8: iconst_1
    9: goto 13
    12: iconst_0
    13: putstatic #21 // Field $assertionsDisabled:Z
    16: return
  run(int) 里 assert 的字节码:
    0: getstatic #21 // Field $assertionsDisabled:Z
    3: ifne 20
    6: iload_0
    7: ifgt 20
    10: new #25 // class java/lang/AssertionError
    14: ldc #27 // String p.Lib: x 必须为正
    16: invokespecial #29 // Method java/lang/AssertionError."<init>":(Ljava/lang/Object;)V
    19: athrow
    20: ldc #32 // String 通过
    0: ldc #7 // class p/Lib
    5: ifne 12

-ea / -da 选项

实测 · 本机执行输出
  [默认(不加选项)]
    assert 没有触发(check 调用次数 0)
    Main.class.desiredAssertionStatus() = false;p.Lib = false;q.Other = false
    p.Lib.run(-5) → 通过;q.Other.run(-5) → 通过
  [-ea]
    assert 失败:java.lang.AssertionError: x 必须为正:-1(check 调用次数 1)
    无消息的 assert:message=null,cause=null
    assert 的详细信息是异常对象:message=java.lang.IllegalStateException: 对象作为详细信息,cause=IllegalStateException
    Main.class.desiredAssertionStatus() = true;p.Lib = true;q.Other = true
    p.Lib.run(-5) → AssertionError(p.Lib: x 必须为正);q.Other.run(-5) → AssertionError(q.Other: x 必须为正)
  [-ea:p...(只打开包 p 及子包)]
    assert 没有触发(check 调用次数 0)
    Main.class.desiredAssertionStatus() = false;p.Lib = true;q.Other = false
    p.Lib.run(-5) → AssertionError(p.Lib: x 必须为正);q.Other.run(-5) → 通过
  [-ea:Main(只打开 Main 类)]
    assert 失败:java.lang.AssertionError: x 必须为正:-1(check 调用次数 1)
    无消息的 assert:message=null,cause=null
    assert 的详细信息是异常对象:message=java.lang.IllegalStateException: 对象作为详细信息,cause=IllegalStateException
    Main.class.desiredAssertionStatus() = true;p.Lib = false;q.Other = false
    p.Lib.run(-5) → 通过;q.Other.run(-5) → 通过
  [-ea -da:p...(全开再关掉 p)]
    assert 失败:java.lang.AssertionError: x 必须为正:-1(check 调用次数 1)
    无消息的 assert:message=null,cause=null
    assert 的详细信息是异常对象:message=java.lang.IllegalStateException: 对象作为详细信息,cause=IllegalStateException
    Main.class.desiredAssertionStatus() = true;p.Lib = false;q.Other = true
    p.Lib.run(-5) → 通过;q.Other.run(-5) → AssertionError(q.Other: x 必须为正)
  [-esa(只打开系统类)]
    assert 没有触发(check 调用次数 0)
    Main.class.desiredAssertionStatus() = false;p.Lib = false;q.Other = false
    p.Lib.run(-5) → 通过;q.Other.run(-5) → 通过
  [-ea:q.Other -da:Main]
    assert 没有触发(check 调用次数 0)
    Main.class.desiredAssertionStatus() = false;p.Lib = false;q.Other = true
    p.Lib.run(-5) → 通过;q.Other.run(-5) → AssertionError(q.Other: x 必须为正)

运行时修改断言状态

实测 · 本机执行输出
  默认选项下:
    p.Lib 还没有被加载时:setClassAssertionStatus("p.Lib", true) 之前 desiredAssertionStatus 未知;设置之后加载它:
      p.Lib.status() = true;p.Lib.run(-5) = AssertionError(p.Lib: x 必须为正)
    p.Lib 已经初始化后再 setClassAssertionStatus("p.Lib", false):p.Lib.run(-5) = AssertionError(p.Lib: x 必须为正)(断言状态在初始化时已经写进 $assertionsDisabled,之后的修改不会改变它)
    setDefaultAssertionStatus(true) 之后才加载的 q.Other:status() = true;run(-5) = AssertionError(q.Other: x 必须为正)

代价

实测 · 本机执行输出
  不加 -ea(断言关闭)             每次迭代 0.58 ns(3 次进程的中位数 [0.58, 0.58, 0.57])
  -ea(断言打开)                每次迭代 10.51 ns(3 次进程的中位数 [10.5, 10.54, 10.51])

分步演示

assert 断言
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;计时是自写循环,3 个进程取中位数
    • 没有验证:-ea 对系统类(-esa)实际打开了哪些断言、断言与模块 / 虚拟线程、assert 在 lambda 与内部类里的 $assertionsDisabled 归属、JDK 27 的行为、IDE / 测试框架默认开启 -ea 的约定(Surefire 默认启用断言,本文没有验证)
    京ICP备2026067502号-1京公网安备11010502063487号