Java Annotations Introduction
What Java annotations are, how they attach metadata to code, and where they're processed.
An annotation is a marker you attach to a piece of Java source — a class, method, field, parameter, type use — that adds metadata. The annotation itself does nothing. It's a label that some other piece of code (the compiler, a framework, an IDE, a build tool) reads later and reacts to. @Override on a method tells the compiler "I'm overriding a superclass method; please yell if I'm not." @Test on a method tells JUnit "this is a test." @Entity on a class tells JPA "map this to a database table." In every case the annotation only carries information; the behaviour lives in the processor that reads it.
You've already seen annotations throughout this book — @Override on overridden methods, @FunctionalInterface on single-method interfaces, @SuppressWarnings to silence the compiler. This part of the book is about the system underneath those: what an annotation is, when it's available (source-only, class file, or runtime), how to write your own, and how processors consume them.
The shape of an annotation
Syntactically, an annotation is the @ symbol followed by the annotation's name, applied immediately before the thing it annotates:
@Override
public String toString() { ... }
@Deprecated
public void oldApi() { ... }
@SuppressWarnings("unchecked")
List<String> list = (List<String>) raw;Some annotations take elements (their version of fields). The element values go in parentheses:
@SuppressWarnings("unchecked") // one element, value "unchecked"
@SuppressWarnings({"unchecked", "rawtypes"}) // array of strings
@RequestMapping(path = "/users", method = GET) // two named elementsIf an annotation declares a single element called value, you can omit the name — @SuppressWarnings("unchecked") is shorthand for @SuppressWarnings(value = "unchecked").
What annotations are not
Three negatives that prevent the most common misunderstandings:
- Annotations don't execute code. Writing
@Cachednext to a method does not cache anything. Something else has to look for@Cachedand add caching behaviour. The annotation is a flag, not a function. - Annotations aren't comments. Comments vanish at compile time; annotations are first-class language constructs. They participate in the type system, they can be required to live in the class file, and they can be read at runtime via reflection.
- Annotations aren't a substitute for clear code. A long stack of annotations atop a class is information density, not always clarity. Frameworks that lean heavily on annotations (Spring, JPA, JAX-RS) pay for the convenience with a learning curve and runtime cost.
When the annotation lives
Every annotation has a retention policy that decides how long the metadata survives:
SOURCE— the compiler reads it then discards it.@Overrideand@SuppressWarningswork this way; the bytecode contains no record of them.CLASS— the annotation is written into the.classfile but not loaded by the JVM at runtime. This is the default. Tools that inspect bytecode (static analysers, post-processors) can read it.RUNTIME— the annotation is kept all the way through; reflection can ask any class, method, or field for its annotations at runtime. Frameworks like Spring and Jackson rely on this.
You'll see the @Retention(...) meta-annotation that sets this policy in the Meta-annotations chapter. The short version: pick retention based on who needs to read the annotation — the compiler, a build-time tool, or runtime code.
Where the annotation can go
Each annotation also has a target — the set of program elements it can legally annotate. The common targets:
TYPE— classes, interfaces, enums.METHOD— methods.FIELD— fields.PARAMETER— method parameters.CONSTRUCTOR— constructors.LOCAL_VARIABLE— local variable declarations.ANNOTATION_TYPE— other annotation declarations (meta-annotations).PACKAGE— packages, viapackage-info.java.TYPE_USE— any use of a type, including generic parameters and casts (Java 8+).
If you put an annotation somewhere its @Target doesn't allow, the compiler refuses. Trying @Override on a class declaration is a compile error because @Override is targeted at METHOD.
Who reads annotations
Three places consume annotation data:
- The compiler. Built-in annotations like
@Override,@SafeVarargs, and@FunctionalInterfaceare checked byjavacitself. - Annotation processors. Pluggable compile-time tools that run during
javac. They can read annotations on the sources being compiled and generate new source files in response. Lombok, Dagger, Hibernate's static metamodel, and theMicronautframework all work this way. - Reflection at runtime.
Method.getAnnotations(),Class.getAnnotation(...), etc. return the annotation instances for any element withRUNTIMEretention. This is how Spring decides what to inject, how JUnit finds your tests, and how Jackson maps JSON.
The first two need no virtual machine support beyond what javac provides. The third needs the annotation written into the class file and loaded by the runtime.
A worked example: inspecting annotations on your own class
The point of this example is to show that @Override, @Deprecated, and @SuppressWarnings look identical in source but behave differently once the class is compiled. The program declares a class with several annotations, then asks reflection what it can actually see.
What to take from the run:
- The class-level loop saw
@DeprecatedonGreeterbut nothing else —@DeprecatedhasRUNTIMEretention.@Overrideand@SuppressWarningsareSOURCE, so the compiler stripped them before the class file was written and reflection can't recover them. - The per-method loop only printed
@DeprecatedonoldHello. Even thoughtoStringwas declared@Overrideandcastwas declared@SuppressWarnings("unchecked"), neither annotation made it to the class file. The information existed at the momentjavacran — that's how the override check happened — and was then discarded. - The retention check spelled this out:
@Overrideand@SuppressWarningscarry@Retention(SOURCE)in their own declarations, while@Deprecatedcarries@Retention(RUNTIME). Retention is a property of the annotation type, not of how it's used. - Reading
@DeprecatedonGreeterviacls.getAnnotation(Deprecated.class)returned a proxy whose element methods (since(),forRemoval()) returned the values written in source. That's the runtime interface to annotation metadata: an instance whose elements are accessor methods. - The takeaway for choosing retention: if the only consumer is
javac(override checks, warning suppression), useSOURCE. If a framework needs to read the annotation while the program runs (DI, ORM, JSON binding), useRUNTIME. The chapter on meta-annotations covers how you declare this for your own annotation types.
What's coming in this part
The remaining chapters in this part take you through:
- The most common built-in annotations in
java.lang—@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface. - The meta-annotations that configure your own —
@Retention,@Target,@Documented,@Inherited,@Repeatable. - Writing custom annotation types and reading them via reflection.
- The compile-time annotation processing API that frameworks use to generate code.
The arc moves from "what annotations the language gives you" to "what annotations you can build on top of them."