Java Type Annotations #2: Constructors

Marcio EndoMarcio EndoJan 19, 2025

In the Java programming language, we can annotate constructor declarations. For example, it is common for dependency injection (DI) libraries to allow the following:

@InjectMyService(FooProcessor processor, BarLogger logger) {  this.processor = processor;  this.logger = logger;}

In the code above, MyService is the constructor of a hypothetical service, and it is annotated with an Inject annotation. The latter indicates to the DI library that it should use the annotated constructor to create the instance of our service.

Constructors and Declaration Annotations

In this blog post, we are not interested in DI libraries. It is just that the Inject annotation is allowed at the constructor declaration. If we were to look at the annotation declaration itself, we would probably find something close to the following:

@Target({METHOD, CONSTRUCTOR, FIELD})@Retention(RUNTIME)public @interface Inject {}

Notice that, among the kind of elements that the annotation is allowed to target, we find CONSTRUCTOR among them. As the Inject annotation targets Java declarations exclusively, when we use it we say that it is a declaration annotation.

Constructors and Type Annotations

Up until recently, I had no idea that Java constructors could also be annotated with type annotations. I used to look at constructors all the time, and still miss the fact that an use of type was right there.

To illustrate, let's declare the following type annotation:

@Target(ElementType.TYPE_USE)@Retention(RetentionPolicy.RUNTIME)@interface T {}

It is an annotation named T which targets types only. Additionally, later in this post, we'll try to access it at runtime, so we've marked it with the RUNTIME retention policy.

Next, let's use it to annotate the constructor of the following Java class:

static class Subject {  public @T Subject() {}  }

The code above compiles without errors. Please note that the Subject class is a nested class and, as we don't want it to be an inner class, we've used the static modifier.

Inspecting the Annotation at Runtime

For completeness, let's write a program that lists the type annotations on the constructor declared earlier:

void main() throws NoSuchMethodException {  final Class<?> type;  type = Subject.class;  final Constructor<?> constructor;  // 1  constructor = type.getDeclaredConstructor();   final AnnotatedType annotatedType; // 2  annotatedType = constructor.getAnnotatedReturnType();     final Annotation[] typeAnnotations; // 3  typeAnnotations = annotatedType.getDeclaredAnnotations();    print("Type annotations are:", typeAnnotations); // 4}private void print(String msg, Annotation[] annotations) {  System.out.println(msg);  for (Annotation annotation : annotations) {    System.out.println(annotation);  }}

In the main method:

  1. From the Subject class object, we obtain the Constructor object representing the constructor.

  2. From the constructor object, we obtain a reference to the AnnotatedType object representing the type of the constructed object.

  3. From the AnnotatedType, we retrieve all annotations directly present on it.

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

When executed, the program produces the following output:

Type annotations are:
@TypeAnnotations2.T()

So, in fact, our constructor is annotated with the type annotation T.

Conclusion

In the Java programming language, constructor bodies must not contain return statements with a value expression. However, similarly to a method that returns, say, a Foo object, constructors also produces a value. The result of invoking a constructor, assuming it completes normally, is a new object instance, whose type is the type of the class where the constructor is declared.

Therefore, in a constructor declaration, there's a use of a type: the type of the object being constructed. And, in Java, we can annotate any use of a type. And annotations on a use of a type are called type annotations.

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