断言消息可以以 lambda 表达式的形式提供,以避免断言通过时产生的计算开销
Java 语言规范:严格求值(Strict Evaluation)
在 Java 语言规范(JLS §15.12.4.2)中明确规定:在调用一个方法之前,必须先计算出该方法的所有实参(Arguments)。
这里的“计算”是指,无论这个实参是一个简单的变量、一个数学表达式,还是一个方法的调用,都必须先得出它的具体值或引用,然后才能把这个值“传递”给被调用的方法。
JVM 字节码:操作数栈的执行顺序
我们把你的代码(错误写法)反编译成 JVM 字节码(类似汇编指令),就能清晰地看到执行顺序:
你的代码:
assertThrows(IllegalArgumentException.class, calculator.divide(5.0, 0.0), msg);
JVM 的字节码执行顺序(伪代码):
1. 加载常量: IllegalArgumentException.class // 准备第1个参数
2. 加载对象: calculator // 准备调用 divide 的对象
3. 加载常量: 5.0 // 准备第1个普通参数
4. 加载常量: 0.0 // 准备第2个普通参数
5. 执行 invokevirtual #Method divide // ⚠️ 关键一步!
// 此时,JVM 真的去执行 calculator.divide(5, 0) 方法
// 因为除以0,JVM 在此处立即抛出异常(Exception),堆栈中断!
6. 加载常量: "message..." // ⚠️ 这一步根本执行不到
7. 执行 invokevirtual #Method assertThrows // ⚠️ 这一步根本执行不到
结论:字节码指令是顺序执行的。JVM 为了调用 assertThrows,必须先“凑齐”三个参数放在操作数栈里。在凑第二个参数时,它必须执行 divide 方法,结果还没凑齐参数,程序就崩溃了,assertThrows 这个方法本身连进入的机会都没有。
换成 Lambda 后,JVM 做了什么?
当你写成 () -> calculator.divide(5.0, 0.0) 时,字节码发生了根本性的变化:
你的代码:
assertThrows(IllegalArgumentException.class, () -> calculator.divide(5.0, 0.0), msg);
JVM 的字节码执行顺序(伪代码):
1. 加载常量: IllegalArgumentException.class // 准备第1个参数
2. 执行 invokedynamic #LambdaMetafactory // 生成一个 Lambda 对象(函数式接口实例)
// 注意:这一步只是“创建”了一个包裹对象,内部持有对 divide 方法的引用指针
// 但绝对没有执行 divide 方法!
3. 加载常量: "message..." // 准备第3个参数
4. 执行 invokevirtual #Method assertThrows // 终于调用了 assertThrows!
// 此时 JVM 跳转到 assertThrows 的代码内部执行
在 assertThrows 的内部代码中(在 JVM 跳转之后),才会有这么一行:
executable.execute(); // 这里才触发调用 () -> calculator.divide(5.0, 0.0)
直到这个 execute() 执行时,JVM 才去执行那个除法,此时它已经处于 assertThrows 方法体内的 try-catch 保护伞之下了。
技术本质总结
对比维度 直接传值 calculator.divide(…) 传入 Lambda () -> calculator.divide(…)
编译结果 生成 invokevirtual 指令,直接调用除法方法 生成 invokedynamic 指令,创建一个 Executable 实例
JVM 执行时机 在调用 assertThrows 之前(作为参数求值)立即执行 只在 assertThrows 内部调用 execute() 时才执行
是否受控 不受控,异常直接抛给当前测试方法,导致测试失败(误报) 受控,异常被 assertThrows 内部的 try-catch 捕获并验证
💡 一句话总结
在 JVM 的眼里,方法参数就是“待计算的普通值”。直接传方法调用,JVM 会先算结果再传参;传 Lambda,JVM 只传一个“函数蓝图”,把这个“计算动作”的主动权交给了 assertThrows 去控制。
更多推荐




所有评论(0)