Java Annotations Demystified

A lot of the "magic" of Java is rooted within annotations, especially when working with libraries and frameworks. Once you create your own and see how it works, you’ll have an easier time developing Java applications.

Simple Example Code on Github

What They Are

Java annotations are just metadata, since they do not change the way your code runs. The body of an annotated method will run the same as it would without it.

But many tools within the Java ecosystem look for annotations and apply additional behavior if specific ones are present. This is why they seem like magic.

Let’s Create Our Own

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleAnnotation {
  String value() default ""; // set if only one value is passed without an attribute name
  String name() default ""; // providing defaults help avoid null checks
  int age() default -1;
}

Notice the open and closing parentheses after the field name.

Retention Policy

Annotations don’t need to stick around after compile time (RetentionPolicy.SOURCE). These annotations are primarily for IDEs, compilers, and code checkers. You can also have annotations included within class files, but not at runtime, which is the default policy (RetentionPolicy.CLASS). This is primarily for bytecode tools. But most of the time, you’ll want to have annotations included at runtime so your libraries or framework will be able to find them and add behavior (RetentionPolicy.RUNTIME).

How Are Annotations Used

In order to do something with an annotation, we use reflection. Reflection refers to the ability to inspect and interact with a class's methods and properties. For our use case, it will help us know if an annotation has been applied to a member of our class.

import java.lang.reflect.Method;

public class Main {
  @SimpleAnnotation(name = "Paul", age = 3)
  public static void main(String[] args) {
    Class<?> clazz = Main.class; // could also use getClass() with an instantiated class object

    for (Method method : clazz.getMethods()) {
      SimpleAnnotation simpleAnnotation = method.getAnnotation(SimpleAnnotation.class);

      if (simpleAnnotation != null) {
        if (!simpleAnnotation.name().isEmpty()) {
          System.out.println(simpleAnnotation.name());
        } else if (!simpleAnnotation.value().isEmpty()) {
          System.out.println(simpleAnnotation.value());
        }

        if (simpleAnnotation.age() > 0) {
          System.out.println(simpleAnnotation.age());
        }
      }
    }
  }
}

Here, I am using reflection to review each method of the Main class to see if my SimpleAnnotation has been applied. If the annotation has been applied, I’ll print the values passed to standard output.

One way or another, the library or framework you're using is looking for annotations in a similar manner and adding behavior.

Special Considerations

It's not obvious what values you must pass to an annotation and what’s optional. Java doesn't give us a way to express that.

For each annotation, you first have to know the annotation exists. Then, you have to read the documentation to really understand what values to pass and what the annotation is doing.

1
Subscribe to my newsletter

Read articles from Paul Pladziewicz directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Paul Pladziewicz
Paul Pladziewicz