January 20, 2024

lealceldeiro

What's a Java annotation interface?

In a previous article we learned about normal interfaces. This begs the question, then, what's a Java annotation interface?

An annotation interface ( §9.6 ) is an interface declared with a distinct syntax, intended to be implemented by reflective representations of annotations ( §9.7 ).

We can say that it's a specialized kind of interface and to differentiate it from a normal interface, the keyword interface is preceded by an at sign (@). For example:


@interface TraceableMethod {
}

Once we have declared (or imported) an annotation interface in our code, we can use it to augment our application, for example, to enhance the Java type system with nullability information. An example of this is Spring's @NonNull annotation.

In this sense, annotations can be used as well with static code analysis tools to detect possible bugs in our code in an early stage and increase productivity in general by avoiding common mistakes in our code and focus in our business logic.

Syntax

From the previous example you're probably already inferring the syntax of this type of interfaces. Let's see it:


{InterfaceModifier} @ interface TypeIdentifier AnnotationInterfaceBody

Where

Body

The structure of the body of an annotation interface ( AnnotationInterfaceMemberDeclaration ) is as follows:


{
    AnnotationInterfaceElementDeclaration
    ConstantDeclaration
    ClassDeclaration
    InterfaceDeclaration
}

Let's analyze AnnotationInterfaceElementDeclaration . The rest of the elements follow the same 1 rules as those described in normal interface declarations.

Annotation interface elements

Annotations (like classes and interfaces) can have attributes. The difference is that they look like methods in the annotation body, even though they're used as normal attributes where the annotation is used. Let's see an example to get a better idea.


@interface TraceableMethod {
    public String traceName() default "TracedMethod";
}

Here, we've defined an attribute (the method-like syntax) in the annotation declaration called traceName. Notice how the syntax is somehow the same but different from fields declared in normal interfaces and classes. For example, the modifier (public), the type (String), and the identifier (traceName) are specified similar to how they'd be specified in a class or interface, but then we have the parenthesis (()) and the default keyword used after it, along the actual attribute default value ("TracedMethod").

How to use annotations?

Once the annotation is created (through the annotation interface declaration) it can be placed on classes, methods, fields, and a few other places, and it must appear immediately before what's being annotated. For example, let's suppose we have the following class.


class Processor {
    void process() {
    }

    String getName() {
        return "processor";
    }
}

Now, let's suppose that the goal of the annotation we just declared (TraceableMethod) is, given a class, to "mark" the methods in that class whose names we want to print when our program starts up. For this to happen we must do two things: first, annotate the methods, and second, when our program starts up, "scan" our code and find these methods annotated as TraceableMethod. Let's see it.

Annotate the methods

For now, we're going to annotate only the process method.


class Processor {
    @TraceableMethod
    void process() {
    }

    String getName() {
        return "processor";
    }
}

Find the methods annotated in our code


public static void main(String[] args) {
    Class clazz = Processor.class;
    Method[] methods = clazz.getDeclaredMethods();

    for (Method method : methods) {
        for (Annotation annotation : method.getAnnotations()) {
            if (annotation instanceof TraceableMethod traceableMethodAnnotation) {
                System.out.printf("""
                                  Method %s annotated as %s - trace name: %s
                                  """,
                                  method.getName(),
                                  TraceableMethod.class.getSimpleName(),
                                  traceableMethodAnnotation.traceName());
            }
        }
    }
}

If you run this example you'll see an empty output. That's because we did not specify the retention policy of the annotation.

There are three values we can use here:

So, because CLASS is the default retention policy our annotation was kept after compilation, but during runtime, it was not taken into account by the JVM, hence the call to method.getAnnotations() returned an empty array. How do we fix it? We change the retention policy... by using a meta-annotation!


@Retention(RetentionPolicy.RUNTIME)
@interface TraceableMethod {
    public String traceName() default "TracedMethod";
}

Now, if you run the same code again you'll se the following output:


Method process annotated as TraceableMethod - trace name: TracedMethod

Specifying annotation attributes

What about the field traceName declared in the body of the annotation? From the previous output we can see it took the default value (TracedMethod); but what if we want to change it. Simple, we can specify it within parenthesis (()) after the annotation name.

Let's annotate the method getName, in Processor, and let's specify for it a different traceName value.


class Processor {
    @TraceableMethod
    void process() {
    }

    @TraceableMethod(traceName = "String returning method")
    String getName() {
        return "processor";
    }
}

When we run again the code, now this is the output:


Method getName annotated as TraceableMethod - trace name: String returning method
Method process annotated as TraceableMethod - trace name: TracedMethod

As you see here, now the method getName is reported with a traceName value different from the default one, that's it, String returning method.

Example: Using the @Override annotation

The previous example is contrived and somehow silly, the code doesn't do anything useful. It was for demonstration purposes. Let's see an example of an annotation that's used very often and which is extremely helpful: @Override .

This annotation provides us with great benefit during compile-time by "forcing" us to properly implement an equals method in our class.

Consider the common error made when overriding, shown in the next code snippet:


class Processor {
    public boolean equals(Processor obj) {
        // return true or false ... omitted for brevity
    }
}

In this code there's one subtle mistake: the argument received in the equals method is of type Processor, but it should be of type Object as per the specs. However, we may not notice it. And the compiler can't help us: it's a completely valid java code; in fact this code creates an accidental overload .

@Override to the rescue! If we annotate the method with @Override, now the compiler knows we're intending to override the Object's equals method and reports an error at compile time (Something like "Method does not override method from its superclass").

This would be the fixed version of the previous code:


@Override
public boolean equals(Object obj) {
    // code omitted for brevity
}

It's worth mentioning that we're not limited to the equals method. We can use this annotation with any method we intend to override from some of the class ancestors.

Do we need annotation interfaces?

Technically speaking, we may not strictly need them to write our day to day software, but it's really difficult for me to imagine any modern Java program that doesn't use some form of annotation. They enhance our code in ways that makes our lives (as developers) much easier than if we had to do the same work programmatically.

Annotations are also used to provide metadata at runtime. For example, the Jakarta Persistence API ( JPA ) uses several annotations from the package jakarta.persistence to work internally and provide all the features it provides. So, it's not only small enhancements we get in our code; we're talking at this point of entire frameworks relying on annotations to function properly. This is something I don't see going away any time soon.

Final thoughts

There's a bunch of articles, official documentation, and books out there that we can read to get more knowledge about annotations. Hopefully, this is a good start for you to increase your knowledge a bit more about the Java platform and sparkle your interest in reading more about interface annotations and other use cases and details not covered in this article.


1: While the structure and most of the "rules" are the same, there are actually some subtle differences. For example, the return type of a method declared in the body of annotation interface must be one of: a primitive type, String, Class or an invocation of Class, an enum class type, an annotation interface type, or an array type whose component type is one of the previously mentioned types. If such return type is different, a compile-time error occurs.