Java Type Annotations #3: Arrays

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 theMap<String, LocalDate>
type. -
B
annotates theString
type. -
C
annotates theLocalDate
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:
-
The
int[][]
type, which is the type of the field itself. -
The
int[]
type, which is the component type of theint[][]
array. -
The
int
type, which is the component type of theint[]
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:
-
We obtain the
Class
object of the class where our field is declared. -
From the class object, we retrieve the
Field
object representing our field. -
From the field object, we get a reference to the AnnotatedType object representing the type usage in the field.
-
While the annotated type represents an array type, we print its name and all annotations directly present on it.
-
We update the
annotatedType
variable with the component type of the array. -
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 theint[][]
type. -
B
annotates theint[]
type. -
C
annotates theint
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 theint[][]
type. -
B
annotates theint[]
type. -
C
annotates theint
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 theint[][]
type. -
B
annotates theint[]
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 theint[][]
type. -
B
annotates theint[]
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.