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

Marcio EndoMarcio EndoJan 12, 2025

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?

  1. The field declaration?

  2. The field type?

  3. 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:

  1. The F annotation targets field declarations only.

  2. The T annotation targets types only.

  3. 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:

  1. We obtain the Class object of our implicitly declared class.

  2. From the class object, we retrieve the Field object representing the value field.

  3. Using the field object, we fetch all annotations directly present on the field itself.

  4. We iterate over these annotations and print their names.

  5. From the field object, we obtain a reference to the AnnotatedType object representing the type usage in the field.

  6. From the AnnotatedType, we retrieve all annotations directly present on the field's type.

  7. 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:

  1. The F annotation applies to the field declaration.

  2. The T annotation applies to the type.

  3. 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 on f if Foo is meta-annotated by @Target(ElementType.FIELD), and a type annotation on int if Foo 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.