Skip to content
Java java5 6 min read

Annotations

Annotations are a way to attach metadata — extra information — to your Java code. They don’t change what your program does directly, but they provide instructions to the compiler, build tools, and frameworks about how to treat the annotated element.

What Is an Annotation?

Think of an annotation as a label you stick on a class, method, field, parameter, or even another annotation. The label itself does nothing — it’s whoever reads the label that takes action. The Java compiler reads @Override and warns you about typos; JUnit reads @Test and knows which methods to run as tests; Spring reads @Component and registers your class as a bean.

Annotations were introduced in Java 5 (along with generics, enums, and varargs).

public class Animal {

    // Tells the compiler: "this overrides a superclass method"
    @Override
    public String toString() {
        return "I am an animal";
    }
}

Note: Annotations start with @. The name immediately follows with no space — @Override, @SuppressWarnings, @Deprecated.

Built-in Java Annotations

Java ships with several annotations you’ll use every day.

@Override

Verifies that a method truly overrides a method from a superclass or interface. If you misspell the method name, the compiler catches it immediately.

class Vehicle {
    public String getType() { return "Vehicle"; }
}

class Car extends Vehicle {
    @Override
    public String getType() { return "Car"; } // compiler checks this is real
}

See Method Overriding for full details.

@Deprecated

Marks an API element as outdated. The compiler emits a warning whenever deprecated code is called.

public class MathUtils {

    /** @deprecated Use {@link #multiply(int, int)} instead */
    @Deprecated
    public int oldMultiply(int a, int b) {
        return a * b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }
}

Tip: Since Java 9, @Deprecated gained two optional elements: since (the version it was deprecated in) and forRemoval (boolean — signals the API will actually be deleted).

@SuppressWarnings

Tells the compiler to silence specific warning categories for that element.

import java.util.ArrayList;

public class LegacyCode {

    @SuppressWarnings("unchecked")   // suppress raw-type warning
    public void addRawItems() {
        ArrayList list = new ArrayList(); // intentionally raw
        list.add("item");
    }
}

Common warning names: "unchecked", "deprecation", "rawtypes", "unused".

@FunctionalInterface

Documents that an interface is intended as a functional interface (exactly one abstract method). The compiler enforces the contract.

@FunctionalInterface
interface Transformer<T> {
    T transform(T input);
    // adding a second abstract method here would be a compile error
}

@SafeVarargs

Suppresses heap-pollution warnings when a method with a generic varargs parameter is safe to use.

import java.util.List;

public class SafeExample {

    @SafeVarargs
    public static <T> List<T> listOf(T... items) {
        return List.of(items);
    }
}

Meta-Annotations — Annotating the Annotations

Meta-annotations live in java.lang.annotation and configure how your custom annotations behave.

Meta-annotationPurpose
@RetentionWhen the annotation is available (source, class, runtime)
@TargetWhich elements the annotation can be applied to
@DocumentedInclude in Javadoc output
@InheritedSubclasses inherit the annotation from a superclass
@RepeatableThe same annotation can appear more than once on one element

@Retention Policies

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

// Available at runtime via Reflection
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable { }
PolicyVisible to compilerPresent in .class fileAvailable at runtime
SOURCEYesNoNo
CLASS (default)YesYesNo
RUNTIMEYesYesYes

Tip: Use RUNTIME when you need Reflection to read the annotation at runtime (e.g., frameworks). Use SOURCE for compile-time-only hints like @Override.

@Target

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

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

Common ElementType values: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_USE (Java 8+), MODULE (Java 9+).

Creating Custom Annotations

You define an annotation with the @interface keyword. Elements inside look like abstract method declarations.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
    int requestsPerSecond() default 100;
    String description()    default "";
}

Use it like this:

public class ApiController {

    @RateLimit(requestsPerSecond = 10, description = "Public search endpoint")
    public String search(String query) {
        return "results for: " + query;
    }
}

Annotation Element Rules

  • Element types must be: primitives, String, Class, an enum, another annotation, or a 1D array of the above.
  • Elements can have default values.
  • An annotation with no elements is called a marker annotation (e.g., @Override).
  • An annotation with a single element named value can be used without the key: @SuppressWarnings("unused") instead of @SuppressWarnings(value = "unused").

Reading Annotations with Reflection

A RUNTIME-retained annotation can be read at runtime using the Reflection API.

import java.lang.reflect.Method;

public class AnnotationReader {

    public static void main(String[] args) throws Exception {
        Method method = ApiController.class.getMethod("search", String.class);

        if (method.isAnnotationPresent(RateLimit.class)) {
            RateLimit limit = method.getAnnotation(RateLimit.class);
            System.out.println("Rate limit: " + limit.requestsPerSecond());
            System.out.println("Desc: "       + limit.description());
        }
    }
}

Output:

Rate limit: 10
Desc: Public search endpoint

Repeatable Annotations (Java 8+)

Sometimes you want to apply the same annotation more than once to a single element. Mark it @Repeatable and provide a container annotation.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Schedules.class)          // name of the container
public @interface Schedule {
    String day();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {          // container holds an array
    Schedule[] value();
}
public class ReportGenerator {

    @Schedule(day = "Monday")
    @Schedule(day = "Friday")
    public void generateWeeklyReport() { /* ... */ }
}

Annotation Processors (Compile-Time)

SOURCE- and CLASS-retained annotations are processed by annotation processors at compile time (using the javax.annotation.processing API). This is how libraries like Lombok generate boilerplate code, and how Dagger generates dependency-injection code — without any runtime overhead.

You register a processor via META-INF/services/javax.annotation.processing.Processor, and javac invokes it automatically.

Note: Writing annotation processors is an advanced topic. Most developers use annotations from frameworks rather than write processors themselves.

Under the Hood

At the bytecode level, annotations with CLASS or RUNTIME retention are stored in the .class file in attributes called RuntimeVisibleAnnotations and RuntimeInvisibleAnnotations. The JVM loads RuntimeVisibleAnnotations into memory so the Reflection API can read them. RuntimeInvisibleAnnotations (class-level retention) are stored on disk but the JVM discards them after loading, so reflection cannot see them.

Annotation instances returned by reflection are not regular objects — they are dynamic proxies (java.lang.reflect.Proxy) whose invocation handler reads the annotation data from the class metadata. This means calling @RateLimit.requestsPerSecond() goes through a proxy dispatch, which is slightly slower than a normal field read. In tight loops you should cache the result rather than reading it via reflection on every iteration.

Annotation processing at compile time happens in rounds. During each round the processor inspects the source model and can generate new source files; the compiler then compiles those files and potentially triggers another round until no new files are generated.

Quick Reference

AnnotationPackagePurpose
@Overridejava.langVerify method override
@Deprecatedjava.langMark obsolete API
@SuppressWarningsjava.langSilence compiler warnings
@FunctionalInterfacejava.langEnforce single-abstract-method
@SafeVarargsjava.langSuppress varargs heap-pollution warning
@Retentionjava.lang.annotationSet annotation lifecycle
@Targetjava.lang.annotationRestrict applicable elements
@Documentedjava.lang.annotationInclude in Javadoc
@Inheritedjava.lang.annotationPropagate to subclasses
@Repeatablejava.lang.annotationAllow multiple uses on one element
  • Reflection API — read annotations and inspect classes at runtime
  • Functional Interfaces@FunctionalInterface in practice with lambdas
  • Generics — annotations and generics often appear together (e.g., @SuppressWarnings("unchecked"))
  • Method Overriding — the most common use of @Override
  • Java 8 Features@Repeatable, @FunctionalInterface, and TYPE_USE targets added in Java 8
  • Custom Exceptions — a common place to combine annotations with inheritance
Last updated June 13, 2026
Was this helpful?