Annotations

Annotations attach structured metadata to Java code that tools, the compiler, and the runtime can read — without changing the program's own logic.

An annotation is a form of metadata you attach to a class, method, field, parameter, or other program element, using an @Name syntax. Annotations carry no direct executable behavior of their own — they are markers and data that the compiler, build tools, frameworks, or your own code (via reflection) can read and act on. Java ships several built-in annotations (@Override, @Deprecated, @SuppressWarnings, @FunctionalInterface) and lets you define custom ones with @interface, controlling exactly where they can be applied and how long they survive past compilation.

What Annotations Are

Syntactically, an annotation looks like @AnnotationName, optionally followed by parentheses containing element-value pairs: @SuppressWarnings("unchecked") or @RequestMapping(path = "/users", method = "GET"). Annotations do not change what a program computes by themselves — a class annotated @Deprecated still runs exactly the same bytecode. Their effect comes entirely from something else reading the annotation: the compiler, an annotation processor at build time, or reflective code at run time.

Annotations are a form of declarative metadata, distinct from comments (which the compiler ignores entirely) and from code (which the compiler executes). They sit in between: the compiler records them in the class file (depending on retention) but does not execute them.

Built-In Annotations

The java.lang package supplies several annotations every Java developer uses regularly:

@Override tells the compiler this method must actually override a superclass/interface method — a typo in the signature becomes a compile error instead of a silent new method. @Deprecated marks an API as discouraged, optionally with since/forRemoval metadata, and triggers compiler warnings at call sites. @FunctionalInterface asserts the interface has exactly one abstract method, so it compiles as an error if that's violated. @SuppressWarnings silences specific compiler warnings in the annotated scope.

@Override is not required for a method to actually override its superclass version — overriding happens based on signature matching alone. @Override exists purely so the compiler can catch you when you think you're overriding but you're not (wrong parameter types, a typo in the method name), which is otherwise a very easy, very silent bug.

class Animal {
    void speak() { System.out.println("..."); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Woof"); }
}

@Deprecated(since = "3.0", forRemoval = true)
class OldApi {
    void legacyMethod() { /* ... */ }
}

@FunctionalInterface
interface Transformer<T, R> {
    R apply(T input);
}

class Demo {
    @SuppressWarnings("unchecked")
    void raw() {
        java.util.List list = new java.util.ArrayList();
        java.util.List<String> strings = list; // would normally warn
    }
}

Defining a Custom Annotation

Custom annotations are declared with @interface. Its body lists elements, which look like abstract methods but define the annotation's attributes; each can have a default value.

Benchmark is a custom annotation restricted to methods (@Target) and readable at run time (@Retention). warmupIterations and category are its elements, each with a default, so @Benchmark alone (no arguments) is also valid.

An element named value gets special syntax sugar: @SuppressWarnings("unchecked") is shorthand for @SuppressWarnings(value = "unchecked") because SuppressWarnings declares a single element literally named value.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Benchmark {
    int warmupIterations() default 5;
    String category() default "general";
}

class Service {
    @Benchmark(warmupIterations = 10, category = "io")
    void loadData() { /* ... */ }
}

Targets: Where an Annotation May Be Applied

@Target, itself a meta-annotation from java.lang.annotation, restricts which kinds of declarations an annotation is legal on. Without it, an annotation is usable almost anywhere. ElementType values include TYPE (class/interface/enum/record), METHOD, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, and TYPE_USE.

Sensitive is only legal on fields and methods; applying it to a class declaration is a compile-time error because TYPE was not listed.

TYPE_USE, added in Java 8, is the most permissive target — it allows an annotation almost anywhere a type is written, including generic type arguments and cast expressions, which enables tools like the Checker Framework to do nullability analysis: @NonNull String name.

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD})
@interface Sensitive { }

class User {
    @Sensitive
    private String password; // OK: FIELD

    @Sensitive
    void authenticate() { }  // OK: METHOD

    // @Sensitive
    // class Nested { }      // compile error: TYPE not in @Target list
}

Retention: How Long the Annotation Survives

@Retention, the other core meta-annotation, controls how far past source code the annotation is kept:

Choosing RUNTIME has a real cost: the JVM must keep and expose this metadata, and reflective annotation lookups are slower than ordinary field/method access. Frameworks that scan classes for annotations (JUnit's @Test, Spring's @Component) require RUNTIME retention; a compiler-only marker like @Override needs only SOURCE.

RetentionPolicy.SOURCE   - discarded by the compiler; exists only in source
                            (e.g. @Override, @SuppressWarnings)
RetentionPolicy.CLASS    - written into the .class file, but not loaded
                            into the JVM at run time (the default if
                            @Retention is omitted; rarely queried directly)
RetentionPolicy.RUNTIME  - written into the .class file AND available via
                            reflection at run time (e.g. @Deprecated,
                            most framework annotations like @Test, @Autowired)

Repeatable Annotations

Before Java 8, the same annotation type could not be applied twice to the same element. @Repeatable (Java 8+) lifts that restriction by requiring you to also declare a 'container' annotation that holds an array of the repeatable one; the compiler wires the two together automatically.

Schedule is repeatable because Schedules acts as its container, declaring a value() element of type Schedule[]. Reflectively, getAnnotationsByType(Schedule.class) on the run method returns both instances, while getAnnotation(Schedules.class) returns the synthesized container directly.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Schedules.class)
@interface Schedule {
    String day();
}

@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
    Schedule[] value();
}

class Job {
    @Schedule(day = "MONDAY")
    @Schedule(day = "FRIDAY")
    void run() { /* ... */ }
}

Meta-Annotations

Annotations that apply to other annotation declarations are meta-annotations. @Target, @Retention, @Repeatable, and @Inherited (which lets a class-level annotation propagate to subclasses when queried reflectively — but only for TYPE targets, and only single inheritance, not interfaces) are the standard ones in java.lang.annotation. @Documented marks that the annotation should appear in generated Javadoc.

@Inherited only affects annotations placed directly on classes and only propagates through the superclass chain; it has no effect on method or field annotations, which are never inherited by overriding/hiding members automatically.

Reading Annotations via Reflection

RUNTIME-retained annotations can be discovered through java.lang.reflect (Class, Method, Field, Parameter all implement AnnotatedElement).

isAnnotationPresent checks for the annotation's presence; getAnnotation retrieves an instance whose element methods (category(), warmupIterations()) return the values supplied at the use site, or the declared defaults.

getDeclaredMethod looks up the compute method by name.

isAnnotationPresent(Benchmark.class) is true only because Benchmark has RUNTIME retention.

getAnnotation returns a dynamic proxy implementing Benchmark, backing its element methods with the stored values.

import java.lang.reflect.Method;

class Report {
    @Benchmark(warmupIterations = 3, category = "cpu")
    void compute() { }
}

class Inspect {
    public static void main(String[] args) throws Exception {
        Method m = Report.class.getDeclaredMethod("compute");
        if (m.isAnnotationPresent(Benchmark.class)) {
            Benchmark b = m.getAnnotation(Benchmark.class);
            System.out.println(b.category() + " x" + b.warmupIterations());
        }
    }
}

Annotation Processing Overview

Annotations with SOURCE or CLASS retention are typically consumed at compile time by annotation processors — plugins registered with javac that scan the source tree for specific annotations and can generate new source files (but not modify existing ones). This is how libraries like Lombok, Dagger, and MapStruct generate boilerplate without any run-time reflection cost. This mechanism (the javax.annotation.processing / java.compiler APIs, Processor, and tools like AbstractProcessor) is covered in full in the dedicated Annotation Processing topic.

Common Use Cases

In practice, annotations show up in a few recurring roles: compiler hints and safety nets (@Override, @SuppressWarnings, @FunctionalInterface); documentation/deprecation signaling (@Deprecated); framework wiring read via reflection at run time (@Test in JUnit, @Component/@Autowired in Spring, @Entity/@Column in JPA); and compile-time code generation (Lombok's @Getter, Dagger's @Inject). Recognizing which category a given annotation falls into tells you when and how it actually takes effect.

Best Practices

Always specify @Target on custom annotations — an untargeted annotation applicable everywhere is easy to misuse. Choose the narrowest retention that does the job: SOURCE if only the compiler/a processor needs it, RUNTIME only if something genuinely inspects it reflectively. Give every element a sensible default when reasonable, so most use sites can omit it. Document custom annotations well, since their effect is invisible without reading whatever consumes them.

Mental Model and Quick Reference

Think of an annotation as a labeled sticky note attached to a piece of code — it does nothing on its own, but any tool that knows to look for that specific note (the compiler, a processor, reflection) can read it and change its own behavior accordingly. The annotation itself never runs.

@interface  - declares an annotation type
@Target     - where it may be applied (METHOD, FIELD, TYPE, ...)
@Retention  - how long it survives (SOURCE, CLASS, RUNTIME)
@Repeatable - allows the same annotation multiple times on one element
@Inherited  - class-level annotation propagates to subclasses (TYPE only)
@Documented - included in generated Javadoc

Key Takeaways

Annotations are metadata, not code — they have no effect unless something (compiler, processor, or reflective code) reads and acts on them.

@Target restricts where an annotation can be used; @Retention controls how long it survives (SOURCE, CLASS, or RUNTIME).

Only RUNTIME-retained annotations are visible via reflection at run time; this has a real performance and memory cost, so choose retention deliberately.

@Repeatable requires a container annotation holding an array, wired together by the compiler.

Common built-ins — @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface — each serve a distinct compiler-facing purpose.

@interface Config { String name; } @interface Config { String name(); }

Annotation elements are declared as no-argument, no-body abstract methods, not fields — 'String name;' is not valid @interface syntax.

Assuming every annotation is visible via reflection.

Check the annotation's @Retention — only RUNTIME-retained annotations appear via isAnnotationPresent/getAnnotation at run time.

SOURCE- and CLASS-retained annotations (including @Override) are invisible to reflection; reflective code that expects to find them will silently get nothing.

Expecting a method-level annotation to be inherited by an overriding subclass method automatically.

Explicitly re-annotate the overriding method if the annotation's behavior should apply to it, since @Inherited only affects class-level (TYPE) annotations.

@Inherited never propagates method or field annotations, and even for classes it only follows the superclass chain, not interfaces.

Does applying @Deprecated to a method change how that method executes?

No. @Deprecated is pure metadata; it triggers compiler warnings at call sites and communicates intent, but the method's bytecode and behavior are unchanged.

What is the difference between RetentionPolicy.CLASS and RetentionPolicy.RUNTIME?

Both write the annotation into the .class file. CLASS retention means the JVM does not expose it via reflection at run time; RUNTIME retention means it does, via Class/Method/Field's getAnnotation methods.

Why is @Override useful if a method overrides its superclass method with or without the annotation?

@Override doesn't change whether overriding happens — it lets the compiler verify your intent, turning an accidental non-override (wrong signature, typo) into a compile error instead of a silently created new method.

How do you allow the same custom annotation to be applied multiple times to one element?

Mark it @Repeatable(ContainerAnnotation.class), and declare a separate container annotation type with a single value() element of the repeatable annotation's array type.

Which ElementType would you use to allow an annotation on generic type arguments and cast expressions?

ElementType.TYPE_USE, introduced in Java 8.

Why should custom annotations generally specify @Target explicitly?

Without @Target, the annotation is legal on virtually any declaration, which invites misuse in places it was never designed for; explicit targeting documents and enforces intended usage.

What happens if you omit @Retention on a custom annotation?

It defaults to RetentionPolicy.CLASS — retained in the class file but not available via reflection at run time.

Write a custom annotation @Todo with a required String element for a description and a RUNTIME retention, restricted to methods.

import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Todo { String value(); } class Cart { @Todo("handle empty cart edge case") void checkout() { } }

Todo declares a single element named value(), so use sites can write @Todo("...") without naming the element. RUNTIME retention plus @Target(METHOD) ensures it is discoverable via reflection only on methods.

What does this print, and why?

It prints false. Marker has SOURCE retention, so the compiler discards it entirely after compilation — it never reaches the class file, so no reflective API can see it at run time.

Write a repeatable @Author annotation (with name() element) usable multiple times on a class, plus its container.

import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Repeatable(Authors.class) @Target(ElementType.TYPE) @interface Author { String name(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface Authors { Author[] value(); } @Author(name = "Alice") @Author(name = "Bob") class Report { }

Author is marked @Repeatable(Authors.class); Authors holds Author[] value(). Report.class.getAnnotationsByType(Author.class) returns both Author instances at run time.

@Retention(RetentionPolicy.SOURCE)
@interface Marker { }

class Thing {
    @Marker
    void go() { }
}

// elsewhere:
Method m = Thing.class.getDeclaredMethod("go");
System.out.println(m.isAnnotationPresent(Marker.class));