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,
@Deprecatedgained two optional elements:since(the version it was deprecated in) andforRemoval(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-annotation | Purpose |
|---|---|
@Retention | When the annotation is available (source, class, runtime) |
@Target | Which elements the annotation can be applied to |
@Documented | Include in Javadoc output |
@Inherited | Subclasses inherit the annotation from a superclass |
@Repeatable | The 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 { }
| Policy | Visible to compiler | Present in .class file | Available at runtime |
|---|---|---|---|
SOURCE | Yes | No | No |
CLASS (default) | Yes | Yes | No |
RUNTIME | Yes | Yes | Yes |
Tip: Use
RUNTIMEwhen you need Reflection to read the annotation at runtime (e.g., frameworks). UseSOURCEfor 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
defaultvalues. - An annotation with no elements is called a marker annotation (e.g.,
@Override). - An annotation with a single element named
valuecan 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
| Annotation | Package | Purpose |
|---|---|---|
@Override | java.lang | Verify method override |
@Deprecated | java.lang | Mark obsolete API |
@SuppressWarnings | java.lang | Silence compiler warnings |
@FunctionalInterface | java.lang | Enforce single-abstract-method |
@SafeVarargs | java.lang | Suppress varargs heap-pollution warning |
@Retention | java.lang.annotation | Set annotation lifecycle |
@Target | java.lang.annotation | Restrict applicable elements |
@Documented | java.lang.annotation | Include in Javadoc |
@Inherited | java.lang.annotation | Propagate to subclasses |
@Repeatable | java.lang.annotation | Allow multiple uses on one element |
Related Topics
- Reflection API — read annotations and inspect classes at runtime
- Functional Interfaces —
@FunctionalInterfacein 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, andTYPE_USEtargets added in Java 8 - Custom Exceptions — a common place to combine annotations with inheritance