Under the hood: assert in Java
How does pre-Java8 compilers/JVMs handle assert? It is not a function, it is a keyword instead, and has a special semantics of executing its test only if asserts are allowed in the JVM (JVM flag “-ea”).
My goal is to create a simple Java assertFunc() method, which like the assert keyword acts only when the -ea flag of the JVM is set, generates the same error when assertion fails and does nothing in case the assertion are disabled at runtime.
First let’s analyze what Java bytecode is generated when asserts are used. The following simple Java class would suffice our needs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package ch03; public class AssertionsTest { public static void main(String[] args) { int i = 9; assert i < getMax() : "i should be less than " + getMax(); } private static int getMax() { System.out.println("Calling getMax()"); return 7; } } |
The System.out’s are there to show the deferred execution of the message concatenation – the second getMax() is only called if the condition of the assert fails (for test – change i to 1). If you run the program with “-ea” VM flag, only then the code for the assert (and the calculation of its arguments) gets executed.
What happens under the hood can be understood by looking at the decompiled byte code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static final boolean $assertionsDisabled; descriptor: Z flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC ... static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #15 // class ch03/AssertionsTest 2: invokevirtual #16 // Method java/lang/Class.desiredAssertionStatus:()Z 5: ifne 12 8: iconst_1 9: goto 13 12: iconst_0 13: putstatic #2 // Field $assertionsDisabled:Z 16: return |
Here we see that once the compiler sees the “assert” keyword in a class, it generates a static final boolean field (“$assertionsDisabled”) which holds the negated value of Class.desiredAssertionStatus() method call.
How is this field used? We need to look at the main() method itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: bipush 9 2: istore_1 3: getstatic #2 // Field $assertionsDisabled:Z 6: ifne 45 9: iload_1 10: invokestatic #3 // Method getMax:()I 13: if_icmplt 45 16: new #4 // class java/lang/AssertionError 19: dup 20: new #5 // class java/lang/StringBuilder 23: dup 24: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 27: ldc #7 // String i should be less than 29: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 32: invokestatic #3 // Method getMax:()I 35: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 38: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 41: invokespecial #11 // Method java/lang/AssertionError."<init>":(Ljava/lang/Object;)V 44: athrow 45: return |
The main() starts with the assertion flag check – if assertions are disabled (flag is true), a jump (instruction 6) is made to the line just after the assert statement, this way skipping the test. The statement after the assert in this case is the return statement (instruction 45). If assertions are enabled, the getMax() function is called and the test gets executed. If the test fails, a java.lang.AssertionError with the given message is created and thrown, calling getMax() for the second time.
A possible Java 8 equivalent is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package ch03; import java.util.function.BooleanSupplier; import java.util.function.Supplier; public class AssertionsTestLambda { final static boolean areAssertionsDisabled; static { areAssertionsDisabled = !AssertionsTestLambda.class.desiredAssertionStatus(); } public static void main(String[] args) { int i = 9; assertFunc(() -> i < getMax(), () -> "i should be less than " + getMax()); } private static int getMax() { System.out.println("Calling getMax()"); return 7; } public static void assertFunc(BooleanSupplier condition, Supplier<string> messageSupplier) { if (!areAssertionsDisabled && !condition.getAsBoolean()) { throw new AssertionError(messageSupplier.get()); } } } </string> |
I have introduced lambdas for the parameters of the assertFunc to take advantage of the deferred execution. If I simply expected a boolean and a string as parameters to the function assertFunc(), they would always get calculated – even if the asserts were disabled.
Conclusion: Assertions in Java are always present in the bytecode (.class file) and modify the structure of the classes that use them.