Java Type Annotations #1: What's Being Annotated?

In the Java programming language, a type annotation is an annotation that can be applied to the use of a type.
For example, when you declare that a class MyList
implements the interface List
,
you are using the List
type.
In such cases, you may annotate its use with a type annotation:
class MyList<E> implements @ReadOnly List<E> { ...}
Type annotations are typically used by static analysis tools to detect programming errors. Annotations can also be applied to Java declarations. In such cases, they are formally referred to declaration annotations.
In certain parts of a Java program, it may not be immediately clear whether an annotation applies to the use of a type or to a declaration. In this blog post, we will discuss how the language determines what's being annotated in these situations.
Example
Consider the following field declaration:
private @T @F @B int value;
It is a private int
field annotated with the T
, F
, and B
annotations.
These annotations are hypothetical and serve no particular purpose;
they are used purely for demonstration.
Let's focus on one of these annotations, say, the F
annotation.
Which element of the program does the F
annotation apply to?
-
The field declaration?
-
The field type?
-
Both?
To answer this question, let's write a Java program.
The Annotations
First, let's declare our annotations:
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@interface F {}@Target(ElementType.TYPE_USE)@Retention(RetentionPolicy.RUNTIME)@interface T {}@Target({ElementType.FIELD, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@interface B {}
Notice the Target
meta-annotation on each annotation.
It specifies the elements of the program that the annotation can be applied to:
-
The
F
annotation targets field declarations only. -
The
T
annotation targets types only. -
The
B
annotation targets both field declarations and types.
Additionally, to ensure these annotations are accessible at runtime,
we've marked all of them with the RUNTIME
retention policy.
Accessing the Annotations at Runtime
Next, we write the following instance main
method
(please note that instance main
methods are a preview feature of Java 23):
void main() throws NoSuchFieldException { final Class<?> type; type = getClass(); // 1 final Field field; field = type.getDeclaredField("value"); // 2 final Annotation[] declarationAnnotations; declarationAnnotations = field.getDeclaredAnnotations(); // 3 print("Field annotations are:", declarationAnnotations); // 4 final AnnotatedType annotatedType; annotatedType = field.getAnnotatedType(); // 5 final Annotation[] typeAnnotations; typeAnnotations = annotatedType.getDeclaredAnnotations(); // 6 print("Type annotations are:", typeAnnotations); // 7}private void print(String msg, Annotation[] annotations) { System.out.println(msg); for (Annotation annotation : annotations) { System.out.println(annotation); }}
In our main
method:
-
We obtain the
Class
object of our implicitly declared class. -
From the class object, we retrieve the
Field
object representing thevalue
field. -
Using the field object, we fetch all annotations directly present on the field itself.
-
We iterate over these annotations and print their names.
-
From the field object, we obtain a reference to the
AnnotatedType
object representing the type usage in the field. -
From the
AnnotatedType
, we retrieve all annotations directly present on the field's type. -
We iterate over these annotations and print their names.
When we execute this program, it produces the following output:
Field annotations are:
@TypeAnnotations1.F()
@TypeAnnotations1.B()
Type annotations are:
@TypeAnnotations1.T()
@TypeAnnotations1.B()
From this output, we observe:
-
The
F
annotation applies to the field declaration. -
The
T
annotation applies to the type. -
The
B
annotation applies to both the field declaration and the type.
It aligns with the value of the Target
meta-annotation specified in each annotation.
It's All in the JLS
The behavior we observed is fully specified in the Java Language Specification (JLS).
Section 9.7.4, titled "Where Annotations May Appear," provides the following explanation:
Whether an annotation applies to the declaration or to the type of the declared entity—and thus, whether the annotation is a declaration annotation or a type annotation—depends on the applicability of the annotation's interface
Earlier in the same section, the JLS provides a clarifying example:
For example, given the field declaration:
@Foo int f;
@Foo
is a declaration annotation onf
ifFoo
is meta-annotated by@Target(ElementType.FIELD)
, and a type annotation onint
ifFoo
is meta-annotated by@Target(ElementType.TYPE_USE)
. It is possible for@Foo
to be both a declaration annotation and a type annotation simultaneously.
It matches what we've observed in our earlier example.
Conclusion
In certain parts of a Java program, an annotation may apply to the declaration itself, to a type, or to both. While we've focused on field declarations in this blog post, the same happens, e.g., to method declarations, to local variable declarations, just to cite two more examples.
In these kind of locations, determining whether a particular annotation is a type annotation,
a declaration annotation, or both depends on the Target
meta-annotation specified on the annotation's definition.
For those interested in the complete source code used in this post, you can find it in this Gist.