Annotations in Java

An annotation attaches metadata to code. It changes nothing by itself; a compiler, a tool or a framework reads it and acts.

Definition

An annotation is metadata attached to a declaration. It does not change behaviour on its own. Something else, the compiler, a build tool or a library at runtime, reads it and decides what to do.

public class Report {

    @Override
    public String toString() {
        return "Report";
    }

    @Deprecated(since = "2.0", forRemoval = true)
    public void oldExport() { }

    @SuppressWarnings("unchecked")
    public void legacy(List raw) { }
}

The built in annotations

AnnotationEffect
@OverrideCompile error if nothing is actually overridden
@DeprecatedWarns callers; forRemoval makes the intent explicit
@SuppressWarningsSilences named compiler warnings in the smallest possible scope
@FunctionalInterfaceCompile error if the interface does not have exactly one abstract method
@SafeVarargsAsserts that a generic varargs method does not pollute the heap
@Override is the one to use without exception. It costs nothing and turns a silent behavioural bug, a mistyped method name, into a compile error.

Declaring your own

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Timed {
    String label() default "";
    boolean warnOnSlow() default true;
}
public class SearchService {

    @Timed(label = "note-search")
    public List<Note> search(String term) {
        return List.of();
    }
}

An annotation type is declared with @interface. Its members look like methods and act as named parameters; a member called value may be supplied without a name.

Meta annotations

Meta annotationControls
@RetentionHow long it survives: SOURCE, CLASS or RUNTIME
@TargetWhere it may be placed: type, method, field, parameter and so on
@InheritedWhether subclasses inherit it, for class level annotations
@DocumentedWhether it appears in generated documentation
@RepeatableWhether it may appear more than once on one element

Retention decides what can read it

PolicyIn the class fileReadable at runtimeExample
SOURCENoNo@Override
CLASSYesNoBytecode tools
RUNTIMEYesYesAnything using reflection

If a framework must see your annotation while the program runs, it has to be RUNTIME. This is the setting people most often get wrong.

Reading annotations with reflection

for (Method method : SearchService.class.getDeclaredMethods()) {
    Timed timed = method.getAnnotation(Timed.class);
    if (timed != null) {
        long start = System.nanoTime();
        // invoke the method here
        long tookMs = (System.nanoTime() - start) / 1_000_000;
        System.out.println(timed.label() + " took " + tookMs + " ms");
    }
}

This is the whole mechanism behind annotation driven libraries: find the annotated elements, read the values, and behave accordingly.

Repeatable annotations

@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Role {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Roles {
    Role[] value();
}
@Role("admin")
@Role("editor")
public void publish() { }

Allowed member types

An annotation member may be a primitive, String, Class, an enum, another annotation, or a one dimensional array of those. Arbitrary object types are not permitted, because the values must be constants recorded in the class file.

Common mistakes

  • Declaring SOURCE retention and then trying to read the annotation at runtime.
  • Omitting @Target, which allows the annotation almost anywhere.
  • Applying @SuppressWarnings to a whole class and hiding genuine problems.
  • Expecting an annotation to do something by itself. Without a processor it is inert.
  • Forgetting that @Inherited applies to classes only, not to methods or interfaces.

Best practices

  • Always use @Override.
  • Keep @SuppressWarnings on the narrowest element possible, with a comment explaining why.
  • Give custom annotations an explicit @Target and @Retention.
  • Provide sensible default values so most uses stay short.
  • Use @Deprecated together with a Javadoc note pointing at the replacement.

Practice

  1. Write an annotation @Retry(times = 3) valid only on methods and readable at runtime.
  2. Why does an annotation with SOURCE retention return null from getAnnotation?
  3. Use reflection to list every method of a class carrying your annotation.
  4. Explain what @FunctionalInterface prevents, with an example that fails to compile.
  5. Make an annotation repeatable and show the container type it needs.

Conclusion

Annotations record intent in a form that tools can read. Choose @Target and @Retention deliberately, remember that something must process them, and use the built in ones consistently.

Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All Java notes →
Java

Records in Java

A record declares an immutable data carrier, and the compiler generates the constructor, accessors, equals, hashCode and toString.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.