1. Overview

In this tutorial, we’re going to get familiar with a seemingly bizarre feature in the Java Programming language: Missing annotations won’t cause any exceptions at runtime.

Then, we’ll dig deeper to see what reasons and rules govern this behavior and what are the exceptions to such rules.

2. A Quick Refresher

Let’s start with a familiar Java example. There is class A, and then there’s class B, which depends on A:

public class A {
}

public class B {
    public static void main(String[] args) {
        System.out.println(new A());
    }
}

Now, if we compile these classes and run the compiled B, it’ll print a message on the console for us:

>> javac A.java
>> javac B.java
>> java B
A@d716361

However, if we remove the compiled A.class file and re-run class B, we will see a NoClassDefFoundError caused by a ClassNotFoundException:

>> rm A.class
>> java B
Exception in thread "main" java.lang.NoClassDefFoundError: A
        at B.main(B.java:3)
Caused by: java.lang.ClassNotFoundException: A
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

This happens because the classloader couldn’t find the class file at runtime, even though it was there during compilation. That’s the normal behavior many Java developers expect.

3. Missing Annotations

Now, let’s see what happens with annotations under the same circumstances. In order to do that, we’re going to change the A class to be an annotation:

@Retention(RetentionPolicy.RUNTIME)
public @interface A {
}

As shown above, Java will retain the annotation information at runtime. After that, it’s time to annotate the class B with A:

@A
public class B {
    public static void main(String[] args) {
        System.out.println("It worked!");
    }
}

Next, let’s compile and run these classes:

>> javac A.java
>> javac B.java
>> java B
It worked!

So, we see that B successfully prints its message on the console, which makes sense, as everything is compiled and wired together so nicely.

Now, let’s delete the class file for A:

>> rm A.class
>> java B
It worked!

As shown above, even though the annotation class file is missing, the annotated class runs without any exceptions.

3.1. Annotation with Class Tokens

To make it even more interesting, let’s introduce another annotation that has a Class<?> attribute:

@Retention(RetentionPolicy.RUNTIME)
public @interface C {
    Class<?> value();
}

As shown above, this annotation has an attribute named value with the return type of Class<?>. As an argument for that attribute, let’s add another empty class named D:

public class D {
}

Now, we’re going to annotate the B class with this new annotation:

@A
@C(D.class)
public class B {
    public static void main(String[] args) {
        System.out.println("It worked!");
    }
}

When all class files are present, everything should work fine. However, what happens when we delete only the D class file and don’t touch the others? Let’s find out:

>> rm D.class
>> java B
It worked!

As shown above, despite the absence of D at runtime, everything is still working! Therefore, in addition to annotations, the referenced class tokens from attributes are not required to be present at runtime either.

3.2. The Java Language Specification

So, we saw that some annotations with runtime retention were missing at runtime but the annotated class was running perfectly. As unexpected as it might sound, this behavior is actually completely fine according to the Java Language Specification, §9.6.4.2:

Annotations may be present only in source code, or they may be present in the binary form of a class or interface. An annotation that is present in the binary form may or may not be available at run time via the reflection libraries of the Java SE Platform.

Moreover, the JLS §13.5.7 entry also states:

Adding or removing annotations has no effect on the correct linkage of the binary representations of programs in the Java programming language.

The bottom line is, the runtime does not throw exceptions for missing annotations, because the JLS allows it.

3.3. Accessing the Missing Annotation

Let’s change the B class in a way that it retrieves the A information reflectively:

@A
public class B {
    public static void main(String[] args) {
        System.out.println(A.class.getSimpleName());
    }
}

If we compile and run them, everything would fine:

>> javac A.java
>> javac B.java
>> java B
A

Now, if we remove the A class file and run B, we’ll see the same NoClassDefFoundError caused by a ClassNotFoundException:

Exception in thread "main" java.lang.NoClassDefFoundError: A
        at B.main(B.java:5)
Caused by: java.lang.ClassNotFoundException: A
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

According to JLS, the annotation does not have to be available at runtime. However, when some other code reads that annotation and does something about it (like what we did), the annotation must be present at runtime. Otherwise, we would see a ClassNotFoundException.

4. Conclusion

In this article, we saw how some annotations can be absent at runtime, even though they’re part of the binary representation of a class.

As usual, all the examples are available over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.