Java Type Annotations #3: Arrays

Marcio EndoMarcio EndoJan 26, 2025

In the Java programming language, annotations add bits of information to elements of a program. Type annotations are those that may be applied to uses of a type in a program. For example, we use the type IOException whenever we declare a method that throws it; threfore, any type listed in the throws clause of a method can be annotated with one or more type annotations.

Type annotations can also be applied to a part of a type. This is possible because, in Java, a type can be composed of other types. For example, the Map<String, LocalDate> type denotes a Map whose keys are String instances, and whose values are LocalDate instances.

Map Example

Let's consider three hypothetical type annotations A, B and C. If we declare a field using the Map<String, LocalDate> type described earlier, we can use these annotations to separately annotate each part of our type:

private @A Map<@B String, @C LocalDate> field;

It is straightforward to figure out which part each annotation applies to:

  • A annotates the Map<String, LocalDate> type.

  • B annotates the String type.

  • C annotates the LocalDate type.

However, if our type is an array, things are no longer as straightforward.

Array Example

Now, let's change the type of our field declaration: instead of a Map, we'll use the "array of array of ints" type:

private @C int @A [] @B [] field;

We've also used the same type annotations describe in the previous section. The field declaration compiles without errors.

Which type does each of our annotations apply to?

Three Distinct Types

Before we try to answer the question, let's take a closer look at the type of our field declaration. To see it more clearly, we will temporarely remove the annotations:

private int[][] field;

We have three distinct types in our field declaration:

  1. The int[][] type, which is the type of the field itself.

  2. The int[] type, which is the component type of the int[][] array.

  3. The int type, which is the component type of the int[] array.

So, when we put our annotations back:

private @C int @A [] @B [] field;

Each annotation applies to one of the three types described earlier.

A Program that Lists the Annotations

Now, back to our question: which type each annotation applies to? Let's write a program that prints the annotation applied to each part of the array type:

public void main() throws NoSuchFieldException {  final Class<?> clazz;  clazz = getClass(); // 1  final Field field;  field = clazz.getDeclaredField("field"); // 2  AnnotatedType annotatedType;  annotatedType = field.getAnnotatedType(); // 3  while (annotatedType instanceof AnnotatedArrayType arrayType) {    print(annotatedType); // 4    annotatedType = arrayType.getAnnotatedGenericComponentType(); // 5  }    print(annotatedType); // 6}

In the instance main method:

  1. We obtain the Class object of the class where our field is declared.

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

  3. From the field object, we get a reference to the AnnotatedType object representing the type usage in the field.

  4. While the annotated type represents an array type, we print its name and all annotations directly present on it.

  5. We update the annotatedType variable with the component type of the array.

  6. The while loop ends when we get to a component type that isn't an array type; we print the name and all annotations on this last type.

For completeness, here's the source code of the print method:

private void print(AnnotatedType annotatedType) {  final Type type;  type = annotatedType.getType();    System.out.println(type.getTypeName());  for (Annotation annotation : annotatedType.getDeclaredAnnotations()) {    System.out.println(annotation);  }}

It prints the name of the annotated type and all annotations directly present on it.

Results

When executed, the program prints:

int[][]
@A()
int[]
@B()
int
@C()

So, in our field declaration:

private @C int @A [] @B [] field;

We have the following:

  • A annotates the int[][] type.

  • B annotates the int[] type.

  • C annotates the int type.

In other words, assuming X is a type annotation, in the following declaration:

private @X int[][] field;

The X annotation applies to the int type only. If we want to annotate the declared type of the field, i.e., the int[][] type, we must write the following instead:

private int @X [][] field;

There's a reason for this choice.

Why?

We can find the reasoning in Java Language Specification (JLS). Section 9.7.4, titled "Where Annotations May Appear", says the following:

An important property of this syntax is that, in two declarations that differ only in the number of array levels, the annotations to the left of the type refer to the same type.

So, in all three of the following field declarations:

private @X int a;private @X int[] b;private @X int[][] c;

The X annotation always annotates the int type.

C-Style Array Declarations

In Java, we can also have "C-style" array declarations, where the brackets goes to the right of the declaration name. For example, all three of the following field declarations have the same array type:

private int[][] field1;private int[] field2[];private int field3[][];

What if we add type annotations to the mix? In other words, if we write the following:

private @C int @A [] @B [] field1;private @C int @B [] field2 @A [];private @C int field3 @A [] @B [];

What's being annotated in each case? Writing a variation of our previous program, we find that, in all three cases:

  • A annotates the int[][] type.

  • B annotates the int[] type.

  • C annotates the int type.

It is worth noticing that, in the second field, named field2, the relative position of the A and the B annotations is different when compared to the other two fields. Why is that?

Why?

Once more, we find the reasoning in the JLS. Section 10.2, titled "Array Variables", explains that our three field declarations could be rewritten like the following:

private int[][] field1, field2, field3;

So, to annotate the int[][] type of the field declarations, we would do:

private int @X [][] field1, field2, field3;

Now, suppose we would like for the field2 to be a three-dimensional array. We could add the brackets to the right of the field name, like so:

private int @X [][] field1, field2[], field3;

So the type of field2 has changed from int[][] to int[][][]. However, the X annotation continues to annotate the same type as before, namely the int[][] type.

Method declarations

The same rules apply to method declarations:

public int @B [] method1() @A [] {  ...  }public int @A [] @B [] method2() {  ...  }

The return type of both methods is the int[][] array. And, in both examples:

  • A annotates the int[][] type.

  • B annotates the int[] type.

Variable Arity (Varargs)

Finally, regarding variable arity parameters, suffice it to say that the following are equivalent:

void m(int @A [] @B []  x) {}void n(int @A [] @B ... y) {}

In other words:

  • A annotates the int[][] type.

  • B annotates the int[] type.

You can find this example in Section 10.2 of the JLS.

Conclusion

Whenever we use an array type in a Java program, we deal with two types: the array itself and its component type. The component type of an array can itself be another array, in which case we have multi-dimensional arrays.

Type annotations can be applied to an array type and to its component type. If the component type is another array, then its component type can also be annotated, and so on.

Figuring out if a type annotation applies to the array type or its component type may not be straightforward at first. Knowing why the syntax rules are designed this way helps avoiding mistakes.

You can find the source code used in this blog post in this Gist.