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.
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
{InterfaceModifier}
is one of:
Annotation
, public
, protected
, private
, abstract
,
static
, sealed
, non-sealed
, strictfp
.
@
is the special sing used to indicate that this is an annotation interface declaration, and not
a normal interface declaration.
interface
is the Java keyword used to specify we're declaring an interface.TypeIdentifier
is the name we want to give the annotation. In our previous example we used TraceableMethod
.
AnnotationInterfaceBody
is the body
of our annotation, including the curly braces ({}
). See next section.
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.
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"
).
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.
For now, we're going to annotate only the process
method.
class Processor {
@TraceableMethod
void process() {
}
String getName() {
return "processor";
}
}
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:
CLASS
(keep the annotation after the code is compiled but ignored by the JVM during runtime)
RUNTIME
(keep the annotation after the code is compiled and by the JVM during runtime)
SOURCE
(discard the annotation by the compiler)
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
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
.
@Override
annotationThe 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.
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.
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.