Java Annotations Explained

 · 8 min
Erda Estremera on Unsplash

With JSR-175, Java 5 gained a metadata facility, allowing us to annotate our code with decorative syntactic metadata.

“an·​no·​ta·​tion | a-nə-ˈtā-shən
1: a note added by way of comment or explanation”
- Merriam-Webster

This metadata can be provided for types, fields, methods, parameters, constructors, local variables, type parameters, usage of types, and even other annotation types. It can be used at different steps of our code’s lifecycle by a wide arrangement of tools.


Anatomy of Annotations

The basic definition of an annotation type is simple:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface MyAnnotation {

    String name() default "";

    int value();
}

Let’s go through it line-by-line, everything will be explained in detail further down:

@Retention
In which lifecycle of our code the annotation will be available.
@Target
Where we can we use the annotation.
@Inherited
If present, an annotated type will pass it on to any subtypes.
@Documented
If present, documentation tools like javadoc can access it.
@interface
Marks an annotation type.

And the values of the annotation, optionally with a default value.

Basic usage

The simplest annotation use would be @MyAnnotation at a compatible target site.

But annotations can have multiple values that might be required to be set if no default value is provided. The value name value() is a special one. It can be used without a name if no other values are present.

java
// Both values are specified, naming them is required
@MyAnnotation(name = "an awesome name", value = 42)
public class MyType { ... }

// Only "value()" is present, "name()" will be its default value
@MyAnnotation(value = 42)
public class MyType2 { ... }

// Only "value()" needed, we can actually omit the name
@MyAnnotation(42)
public class MyType3 { ... }

@Retention

The typical lifecycle of our code is as follows:

Source Code
   ▼
   ▼ ◁ Compiler
   ▼
Class file
   ▼
   ▼ ◁ JVM
   ▼
Runtime

The retention policy of annotations reflects these lifecycles and provides us with a way to specify the exact availability of metadata:

  • RetentionPolicy.SOURCE
    Annotations are only available in the source. The compiler will discard the metadata, so neither the compiler nor runtime has access to it. This retention policy is useful for pre-compile tools, like annotation processors.
  • RetentionPolicy.CLASS
    The default retention policy. Annotations are visible to the compiler, and will be available in the class files, but not at runtime. Any post-compile byte-code tools might use the metadata.
  • RetentionPolicy.RUNTIME
    All metadata will be available at runtime.

Which retention policy we need for our custom annotations depends on our requirements.

The provided metadata might contain sensitive information on the inner workings of the annotated code. We should always choose the lowest retention possible for our code to still work.

@Target

Not every annotation makes sense on every available target. That’s why we can explicitly set the acceptable targets. The eight available targets are defined in java.lang.annotation.ElementType:

  • ElementType.PACKAGE
    Package declarations.

  • ElementType.TYPE
    Classes, interfaces, enum.

  • ElementType.TYPE_PARAMETER
    Generic type parameters. Available since Java 8.

  • ElementType.TYPE_USE
    Any usage of a type, like declarations, generic parameters, or casts. Available since Java 8.

  • ElementType.ANNOTATION_TYPE
    Annotation types.

  • ElementType.CONSTRUCTOR
    Constructor declaration.

  • ElementType.FIELD
    Fields and enum constants.

  • ElementType.METHOD
    Method declarations.

  • ElementType.LOCAL_VARIABLE
    Local variable declarations (not retained in class files or at runtime).

The @Target annotation accepts an array of targets:

java
// Multi-Target
@Target({ ElementType.FIELD, ElementType.Type })

// Single-Target
@Target(ElementType.ANNOTATION_TYPE)

If @Target is not specified, the annotation defaults to every available ElementType``, except ElementType.TYPE_PARAMETER`.

@Inherited

Annotations are not inherited by default. By adding @Inherited to an annotation type, we allow it to be inherited. This only applies to annotated type declarations, which will pass it down to their subtypes.

java
@MyAnnotation(value = 42)
public class MyType { ... }

// Any annotation check at runtime
// will also provide "MyAnnotation" and its value 42
public class MySubType extends MyType { ... }

@Documented

Java default behavior for documentation is to ignore any annotation. With @Documented we can change this, making the metadata and its values accessible through documentation.

@Repeatable

Until Java 8, we could apply a specific annotation type only once on a target. With the help of the annotation @Repeatable, we can now declare an annotation repeatable by providing an intermediate annotation:

java
public @interface MyRepeatableAnnotationContainer {
    MyRepeatableAnnotation[] value();
}

@Repeatable(MyRepeatableAnnotationContainer.class)
public @interface MyRepeatableAnnotation {
  String value() default "";
}

Now we can use our annotation more than once:

java
@MyRepeatableAnnotation
@MyRepeatableAnnotation("Foo")
@MyRepeatableAnnotation("Bar")
public class MyType { ... }

Annotation Values

Being able to annotate our code and check if the annotation is present at different lifecycle events is great. But providing additional values besides the annotation type itself is even better. And even default values are supported.

Values are optional, separating annotations into two groups:

  • Marker — No values. The mere presence is the actual metadata. Examples: @Documented, @Inherited, @Override.
  • Configuration — Values present, maybe with default values for less typing when used. Examples: @Target, @Retention.

The Java Language Specification (JLS) splits Configuration into Normal Annotation and Single Element Annotation. But in my opinion, the behavior of those two overlaps enough to be treated as (almost) equal.

Configuration annotations support multiple values. The allowed types are defined in the JLS 9.6.1:

  • Primitive types
  • String
  • The type Class or Class<T>
  • Enum types
  • Annotation types
  • Array of any preceding type (single-dimension only)

Arrays are handled uniquely. If only a single value is provided when used, we can omit the curly braces.

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Values {

    String name() default "";

    int value();

    Class allowedTypes() default String.class,

    ElementType[] types();
}

Default values must be constant expressions, although null is not acceptable. Arrays can return an empty array by using {} as their default value.


Built-In Annotations

The JDK includes multiple annotations besides the ones we already encountered for creating annotation types themselves:

  • @Override
    Indicates that a method overrides/replaces an inherited method. This information is not strictly necessary, but it helps to reduce mistakes. If we want to override a method but have a simple type in the signature, or the wrong argument type, that error might go unnoticed. But if we provide an @Override annotation, the compiler makes sure we actually override a method, and not just accidentally add or overload it.
  • @Deprecated
    Another compile-only annotation. We can mark code as deprecated, and the compiler/IDE can access this information to tell us the code isn’t supposed to be used anymore. Since Java 9, this previous marker annotation becomes a configuration annotation. The values String since() default "" and boolean forRemoval() default false were added to provide even more info for compilers and IDE to work with.
  • @FunctionalInterface
    Since Java 8, we can mark interfaces to be single abstract method interfaces (SAM), so they can be used as lambdas. This marker annotation allows the compiler to ensure that an interface has precisely one single abstract method. f we add another abstract method, our code will no longer compile. This annotation enables the compiler-check, but isn’t strictly necessary. Any SAM is automatically a functional interface.
  • @SafeVarargs
    Another “trust me, I’m an engineer” marker annotation. Tells the compiler that we won’t do any unsafe operation when using varargs.
  • @SuppressWarnings
    A configuration annotation, accepts an array of warning names that should be disabled during compilation.

How to Access Annotations at Runtime

Adding metadata isn’t enough. We also need to access it somehow. Thanks to reflection, we can access it via the class-object.

Check for annotation

java
Class<MyAnnotatedType> clazz = anInstance.getClass();
// or: Class<MyAnnotatedType> clazz = MyAnnotatedType.class;

// Target: Package
boolean isTypeAnnotationPresent =
    clazz.getPackage().isAnnotationPresent(MyAnnotation.class);

// Target: Type
boolean isTypeAnnotationPresent =
    clazz.isAnnotationPresent(MyAnnotation.class);

// Target: Method
Method method = clazz.getMethod("myMethod");
boolean isMethodAnnotationPresent =
    method.isAnnotationPresent(MyAnnotation.class);

// Target: Field
Field field = clazz.getField("myField");
boolean isFieldAnnotationPresent =
    field.isAnnotationPresent(MyAnnotation.class);

Access metadata

Equivalent to boolean isAnnotationPresent(Class<? extends Annotation> annotationClass), we also have methods for accessing the actual annotation instance, providing us with access to its values.

Here are some of the methods available to different targets:

Classes

Methods


Use Cases

An excellent use case is Serialization. With annotations, a lot of additional metadata can be provided on how our data structures should be processed.

Jackson, a JSON Serialization framework, uses the @JsonProperty annotation to provide every information necessary to modify the default Serialization process:

java
class SimplePojo implements Serializable {

    private String name;

    @JsonProperty(value = "json_name",
                  required = true,
                  access = ACCESS.READ_ONLY)
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Another excellent use case is how [RESTEasy](https://resteasy.github.io/ uses annotations to describe REST endpoints, so no additional config is needed elsewhere:

java
@Path("/1.0/login")
@Produces("application/json")
public class LoginResource {

    @POST
    @Path("/")
    Response login(@FormParam("username") String username,
                   @FormParam("password") String passowrd,
                   @HeaderParam("User-Agent") String userAgent) {
                       ...
                   }

    @HEAD
    @Path("/")
    Response ping() {
        ...
    }
}

This way, RestEASY can perform routing (@Path), validates allowed HTTP methods (@POST and @HEAD), provides data extracted from the request (@FormParam and @HeaderParam), and uses a defined media type for the response (@Produces).

All without any additional config-file or objects. The configuration is right there in the corresponding code.


Conclusion

Annotations are a great way to provide additional data, either for ourselves or third-party tools and libraries. But be aware of the additional costs of parsing, compiling, and lookup of annotations, especially at runtime.


Resources