Java Type Annotations #4: Qualified Names and Nested Types

In the Java Programming language, type annotations are those which can be applied to the use of a type. We use types whenever we declare a field in a Java class, for example. As a result, we can use type annotations to annotate the type of a field declaration.
Types in Java can be nested.
In other words, we can write a type declaration within the body of another type declaration.
For example, the java.util.Map
interface declares the nested Entry
interface.
The latter represents a map entry, i.e., a single key-value mapping.
When referring to a type, we can use a simple name or we can use a qualified name.
For example, if we import the java.util.Map.Entry
type we can use its simple name Entry
when referring to it.
If, on the other hand, we import its enclosing type java.util.Map
, we can use the Map.Entry
qualified name.
In this blog post, we'll discuss the interaction between all the forementioned elements.
Example
If X
is an annotation which targets types only,
we can use it to annotate the type of a field declaration which stores a java.util.Map.Entry
instance:
private @X Entry<String, LocalDate> entry;
The X
annotation applies to the type of field, namely the Entry<String, LocalDate>
type,
a map entry whose keys, of the String
type, are mapped to values of the LocalDate
type.
Let's suppose that, instead of referring to the Entry
type by its simple name,
we'd like to use the Map.Entry
qualified name instead.
It could be that our program declares a Calendar.Entry
type;
and we want to make it clearer that our entry
field is a map entry, not a calendar entry.
So, we change our field declaration to the following:
// compilation error!private @X Map.Entry<String, LocalDate> entry;
Our field declaration now fails to compile! Here's the JDK 23 compiler error message:
TypeAnnotations4.java:23: error: type annotation @X is not expected here
private @X Map.Entry<String, LocalDate> entry;
^
(to annotate a qualified type, write Map.@X Entry<String,LocalDate>)
1 error
As per the error message, we should move the annotation immediately before the Entry
simple name:
private Map.@X Entry<String, LocalDate> entry;
Our field declaration now compiles without errors.
Inner Classes
From the example of the previous section, it's possible for one to wrongly assume that the enclosing type cannot be annotated. But the enclosing type can be annotated: when the nested type is an inner class, i.e, a non-static nested class.
Suppose we have the following class hierarchy:
class Top { class Inner {} }
In the code above:
-
Top
is a top-level class. -
Inner
is an inner class relative toTop
.
If X
and Y
are both type annotations, all of the following are valid field declarations:
private @X Top.Inner inner1;private Top.@Y Inner inner2;private @X Top.@Y Inner inner3;
In other words, we can annotate:
-
The enclosing class only.
-
The inner class only.
-
Both the enclosing and inner classes.
Why are we allowed to annotated both types when dealing with inner classes?
Why?
An instance of an inner class is always associated to an instance of its enclosing class. Putting it differently, to create an instance of an inner class, we must first create an instance of its outer class:
Top top = new Top();Inner inner = top.new Inner();
Therefore, whenever we use the type of an inner class, we always have two distinct types:
-
The type of the instance of inner class itself.
-
The type of the associated instance of its enclosing (outer) class.
And, since we are using two distinct types, we can annotate each one separately.
Fully Qualified Names
At times, we must refer to a type by its fully qualified name. It is required, for example, if we use, in the same compilation unit, two distinct types with the same simple name.
One classical example is using both java.util.Date
and java.sql.Date
in the same compilation unit.
Granted, the introduction of the Date/Time API might have, hopefully, reduced its occurrence.
But it still works fine as an example:
import java.util.Date;public class Example { private Date simple; private java.sql.Date full;}
In the Example
class above, we have two fields of two distinct types:
-
The type of the
simple
field was declared with a simple nameDate
. From the import declaration, we know that the full name of its type isjava.util.Date
. -
The type of the
full
field, on the other hand, was declared with the fully qualified namejava.sql.Date
.
Next, using our X
annotation, let's annotate the types of both of our fields.
At first, we might think that the following would be correct:
// compilation error!private @X Date simple;private @X java.sql.Date full;
But our second field fails to compile. Here's the JDK 23 compiler error message:
TypeAnnotations4.java:45: error: type annotation @X is not expected here
private @X java.sql.Date full;
^
(to annotate a qualified type, write java.sql.@X Date)
1 error
As per the error message, we should declare our second field like so:
private @X Date simple;private java.sql.@X Date full;
The annotation goes immediately before the simple name. Our field declaration now compiles without errors.
Conclusion
Type annotations always refer to the type that is closest to the annotation.
If we use a qualified name to refer to a static nested type, such as Map.Entry
,
then Map
in this context does not denote a type: it is just a name.
Therefore, to annotate the Map.Entry
type we write:
-
Map.@X Entry
.
Similarly, to annotate a type referred by its fully qualified name, we must place the annotation immediately before the simple name. In other words, we must not annotate the package name; we should write:
-
java.sql.@X Date
.
When referring to the type of an inner class, we are implicitly referring to two distinct types: the type of the inner class itself and the type of the outer class. It means we can write:
-
Top.@Y Inner
. -
@X Top.Inner
. -
@X Top. @Y Inner
.
All these rules are specified in the Java Language Specification (JLS).
You can find the source code used in this blog post in this Gist.