Annotations
Reviewed & published by Brayan K
By the end of this lesson you'll be able to use Java's built-in annotations correctly, define your own with @interface, control them with meta-annotations, and read them back at runtime with reflection — the exact machinery behind Spring, JPA, and JUnit.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should be comfortable with the Reflection API (reading classes and methods at runtime) and Interfaces (because annotations are declared with the similar-looking @interface keyword). No Spring or JUnit experience needed — you'll build a tiny version of each here.
What You'll Learn
- Use the built-in annotations: @Override, @Deprecated, @SuppressWarnings, @FunctionalInterface
- Define your own annotation with @interface
- Control behaviour with meta-annotations: @Retention, @Target, @Documented, @Inherited
- Add elements with default values and read them back
- Read annotations at runtime with the Reflection API
- Pick the right @Retention so frameworks can actually see your annotation
The Big Idea — Sticky Notes for Your Code
💡 Analogy: An annotation is a sticky note you attach to your code. The note doesn't change what the code does — it's an instruction for whoever reads it later. @Override is a note to the compiler: "double-check this overrides something." @Test is a note to JUnit: "run this as a test." @Entity is a note to Hibernate: "this maps to a database table."
Annotations start with @ and sit directly above the thing they describe — a class, method, field, or parameter. On their own they are passive metadata. Something else — the compiler, a framework, or your own reflection code — has to read the note and act on it. This lesson covers both halves: writing the notes, and reading them.
1️⃣ Built-in Annotations
Java ships with a handful of annotations the compiler already understands. You'll use these four constantly:
| Annotation | Put it on | What it does |
|---|---|---|
| @Override | A method | Compile error if it doesn't override a parent method |
| @Deprecated | Anything | Warns callers it's obsolete and may be removed |
| @SuppressWarnings | Anything | Silences a named warning, e.g. ("unchecked") |
| @FunctionalInterface | An interface | Compile error unless it has exactly one abstract method |
The key thing: these are checked by the compiler, not at runtime. They catch mistakes early. The worked example below uses all four — read it, then run it.
public class Main {
static class Animal {
String speak() { return "..."; }
}
static class Dog extends Animal {
@Override // compiler verifies this really overrides Animal.speak()
String speak() { return "Woof!"; }
}
@Deprecated // anyone who calls this gets a compiler warning
static String oldFormat() { return "v1 (legacy)"; }
static String newFormat() { return "v2 (modern)"; }
// @FunctionalInterface enforces EXACTLY ONE abstract method at compile time
@FunctionalInterface
interface Transformer {
String apply(String input);
}
public static void main(String[] args) {
// @SuppressWarnings silences a specific warning for this one statement.
@SuppressWarnings("unused")
int debugCounter = 0; // we never read it; the warning is hidden
Animal dog = new Dog();
System.out.println("@Override : dog.speak() -> " + dog.speak());
System.out.println("@Deprecated: oldFormat() -> " + oldFormat() + " (callers warned)");
System.out.println(" newFormat() -> " + newFormat() + " (no warning)");
// Because Transformer is a @FunctionalInterface we can use a lambda:
Transformer upper = String::toUpperCase;
System.out.println("@FunctionalInterface: upper.apply(\"hi\") -> " + upper.apply("hi"));
}
}2️⃣ Defining Your Own Annotation
You create an annotation with @interface (one word, with the @). Inside, each "method" declares an element — a piece of data the annotation can carry. Give an element a default and it becomes optional.
@interface Test {
String name() default "unnamed"; // optional — has a default
boolean enabled() default true; // optional — has a default
String[] tags(); // REQUIRED — no default, caller must supply
}Applying it looks like a function call: @Test(name = "addition", tags = {"math"}). If an annotation has a single element named value, you can drop the name: @Role("ADMIN") instead of @Role(value = "ADMIN").
3️⃣ Meta-Annotations — Annotations on Annotations
A meta-annotation describes how your annotation behaves. These two are the ones you must get right:
| Meta-annotation | Controls | Common values |
|---|---|---|
| @Retention | How long it survives | SOURCE, CLASS (default), RUNTIME |
| @Target | Where it may be used | TYPE, METHOD, FIELD, PARAMETER, ... |
| @Documented | Appears in Javadoc | Marker (no value) |
| @Inherited | Subclasses inherit it | Class-level annotations only |
@Inherited means: if a parent class carries the annotation, subclasses are treated as if they carry it too — but only for annotations placed on classes, not on methods or fields.
4️⃣ Reading Annotations at Runtime
This is where annotations earn their keep. Reflection lets you walk a class's methods and fields and ask each one "do you have this annotation?" — exactly how a framework discovers what to do.
for (Method m : MyClass.class.getDeclaredMethods()) {
Test t = m.getAnnotation(Test.class); // null if @Test is not present
if (t != null) { // it's there
System.out.println(t.name()); // read an element value
}
}The same pattern works for classes (MyClass.class.getAnnotation(...)) and fields (field.getAnnotation(...)). Use isAnnotationPresent(X.class) when you only need a yes/no and don't care about the values.
🎯 Your Turn #1 — Define an annotation
Fill in the three blanks so @Route is readable at runtime, restricted to methods, and gives method a default of "GET". Run it and check the output.
import java.lang.annotation.*;
import java.lang.reflect.Method;
public class Main {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Make this annotation readable via reflection at runtime
@Retention(RetentionPolicy.___) // 👉 SOURCE, CLASS, or RUNTIME?
// 2) Restrict it so it can ONLY go on methods
@Target(ElementType.___) // 👉 TYPE, METHOD, FIELD, ...?
@interface Route {
// 3) Give "method" a default of "GET"
String method() default ___; // 👉 a String literal in "double quotes"
String path(); // no default -> caller MUST supply it
}
static class Api {
@Route(path = "/home") // method uses the default
void home() {}
@Route(method = "POST", path = "/login") // method overrides the default
void login() {}
}
public static void main(String[] args) {
for (Method m : Api.class.getDeclaredMethods()) {
Route r = m.getAnnotation(Route.class);
if (r != null) {
System.out.println(r.method() + " " + r.path());
}
}
}
// ✅ Expected output (order may vary — getDeclaredMethods has no fixed order):
// GET /home
// POST /login
}🎯 Your Turn #2 — Read an annotation
The @Role annotation is already defined. Fill in the two blanks to read it off each method and print only the methods that carry it.
import java.lang.annotation.*;
import java.lang.reflect.Method;
public class Main {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Role {
String value(); // single element named "value"
}
static class Admin {
@Role("ADMIN") void deleteUser() {}
@Role("USER") void viewProfile() {}
void ping() {} // no @Role
}
public static void main(String[] args) throws Exception {
for (Method m : Admin.class.getDeclaredMethods()) {
// 🎯 YOUR TURN — read the @Role annotation off this method
// 1) Get the annotation (will be null when @Role is absent)
Role r = m.___(Role.class); // 👉 which Method method reads ONE annotation?
// 2) Only print methods that actually have @Role
if (r ___ null) { // 👉 "is not equal to" operator
System.out.println(m.getName() + " requires role " + r.value());
}
}
}
// ✅ Expected output (order may vary):
// deleteUser requires role ADMIN
// viewProfile requires role USER
}🧩 Mini-Challenge — A tiny @NotNull validator
Support is faded now — only a comment outline is given. Build a field-level annotation and a reflection loop that checks required fields, like Bean Validation does. The expected output is in the comments.
import java.lang.annotation.*;
import java.lang.reflect.Field;
public class Main {
// 🎯 MINI-CHALLENGE: a tiny "@NotNull required field" validator
//
// 1. Define an annotation @NotNull
// - @Retention(RUNTIME) (so reflection can see it)
// - @Target(ElementType.FIELD) (it goes on fields)
// 2. Make a class User with two String fields:
// - @NotNull String username (set it to "ada")
// - String nickname (leave it null, no annotation)
// 3. In main: loop over User.class.getDeclaredFields().
// For each field that has @NotNull, read its value (f.setAccessible(true);
// f.get(userInstance)). If the value is null, print an error line.
//
// ✅ Expected output (username="ada", nickname=null):
// Validating User...
// OK: username
// (no error, because the only @NotNull field is set)
// your code here
}Common Errors
- ❌ Wrong retention — annotation invisible at runtime. You define an annotation and getAnnotation() always returns null. The cause is the default @Retention(RetentionPolicy.CLASS), which discards it before runtime. Fix: add @Retention(RetentionPolicy.RUNTIME) to the @interface.
- ❌ Wrong target — won't compile. error: annotation type not applicable to this kind of declaration means you put the annotation somewhere its @Target forbids — e.g. a @Target(ElementType.METHOD) annotation placed on a field. Fix: add the right ElementType to @Target (you can list several: {METHOD, FIELD}) or move the annotation.
- ❌ Forgetting a default on a mandatory element. error: annotation @Test is missing a default value for the element 'tags' appears when you apply the annotation without supplying a non-default element. Fix: either pass the element when you use it, or add default to its declaration to make it optional.
- ❌ Confusing @interface with interface. Writing interface Test (no @) defines a regular interface, and @Test above a method won't compile. Fix: use @interface to declare an annotation.
- ❌ Non-constant element value. error: element value must be a constant expression happens with @Test(name = someVariable). Fix: pass a literal, enum, or X.class — never a variable or method call.
Where You've Already Seen This
- 💡 Spring: @Component, @Autowired, @RequestMapping, @Transactional — the framework scans for these with the same reflection you just used.
- 💡 JPA / Hibernate: @Entity, @Table, @Column, @Id — map Java objects to database tables.
- 💡 JUnit 5: @Test, @BeforeEach, @DisplayName — your worked example was a miniature version of this.
- 💡 Lombok: @Data, @Builder, @Getter — these use a SOURCE-retention annotation processor to generate code at compile time.
📋 Quick Reference
| Task | Syntax | Notes |
|---|---|---|
| Define annotation | @interface Name { } | Note the @ — not plain interface |
| Element + default | String name() default "x"; | default makes it optional |
| Keep at runtime | @Retention(RetentionPolicy.RUNTIME) | Required for reflection |
| Restrict placement | @Target(ElementType.METHOD) | TYPE, FIELD, PARAMETER, ... |
| Read one annotation | m.getAnnotation(X.class) | null if absent |
| Check presence | m.isAnnotationPresent(X.class) | Returns a boolean |
| Apply with values | @Test(name = "add") | Looks like a method call |
🎉 Lesson Complete!
You can now read the built-in annotations the compiler enforces, define your own with @interface, steer them with @Retention and @Target, give elements defaults, and pull them back out at runtime with reflection. That last loop — scan, read, act — is the whole secret behind Spring, JPA, and JUnit.
Practice quiz
Which keyword declares a custom annotation?
- interface
- annotation
- @interface
- @annotation
Answer: @interface. @interface (with the @) declares an annotation. Plain interface declares a normal interface.
To read a custom annotation with reflection at runtime, it MUST be marked:
- @Retention(RetentionPolicy.RUNTIME)
- @Retention(RetentionPolicy.SOURCE)
- @Retention(RetentionPolicy.CLASS)
- @Documented
Answer: @Retention(RetentionPolicy.RUNTIME). Only RUNTIME retention survives to runtime. The default is CLASS, which is discarded before the program runs.
What is the DEFAULT retention policy if you don't specify @Retention?
- SOURCE
- RUNTIME
- There is no default
- CLASS
Answer: CLASS. The default is CLASS: kept in the .class file but not visible to reflection at runtime.
What does @Target control?
- How long the annotation survives
- Where the annotation may be applied (method, field, type, ...)
- Whether subclasses inherit it
- Whether it appears in Javadoc
Answer: Where the annotation may be applied (method, field, type, ...). @Target restricts placement (METHOD, FIELD, TYPE, etc.); @Retention controls how long it survives.
An annotation element declared as String name() default "unnamed"; is:
- Optional — callers may omit it and get "unnamed"
- Mandatory at every use
- Read-only and uneditable
- Illegal syntax
Answer: Optional — callers may omit it and get "unnamed". A default makes the element optional. Without a default, every use must supply a value or it won't compile.
If a method has NO @Test annotation, what does m.getAnnotation(Test.class) return?
- A blank Test instance
- An empty Optional
- null
- It throws an exception
Answer: null. getAnnotation returns null when the annotation is absent. Use isAnnotationPresent for a yes/no check.
What does @Override actually do?
- Changes the method's behaviour at runtime
- Tells the compiler to verify the method really overrides a parent method
- Makes the method run faster
- Marks the method as deprecated
Answer: Tells the compiler to verify the method really overrides a parent method. @Override is a compile-time check: if you mistype the name or parameters, you get an error instead of a silent bug.
Annotation element values must be:
- Any expression including method calls
- Always Strings
- References to local variables
- Compile-time constants (literals, enums, class literals, arrays of those)
Answer: Compile-time constants (literals, enums, class literals, arrays of those). Element values must be constant expressions; you cannot pass a variable or a method call.
@FunctionalInterface causes a compile error unless the interface has:
- At least two abstract methods
- Exactly one abstract method
- No methods
- A default method
Answer: Exactly one abstract method. @FunctionalInterface enforces exactly one abstract method, allowing the interface to be used with a lambda.
The reflection pattern frameworks like Spring and JUnit use to act on annotations is:
- Compile the annotation away
- Ask the user at runtime
- Scan members, read the annotation, then act on it
- Replace the bytecode
Answer: Scan members, read the annotation, then act on it. Loop over methods/fields, read each annotation via reflection, and react — the same scan-read-act loop you build here.
Continue this course
- Previous: Reflection API & Dynamic Code Analysis
- Next: I/O Streams & NIO2 for High-Performance File Handling — Read/write files efficiently with NIO2, Paths, and FileChannel
- Quick reference: Java cheat sheet
Frequently asked questions
What is the difference between @interface and interface in Java?
interface defines a contract of methods a class must implement. @interface (with the @ symbol) defines an annotation — a type used to attach metadata to code. They look similar but are completely different: you implement an interface, but you apply an annotation with @Name above a class, method, or field.
Why can't I read my custom annotation with reflection?
Almost always because the annotation is missing @Retention(RetentionPolicy.RUNTIME). The default retention is CLASS, which keeps the annotation in the .class file but discards it before runtime, so getAnnotation() returns null. Add @Retention(RetentionPolicy.RUNTIME) to your @interface and reflection will see it.
What does @Override actually do if it doesn't change behaviour?
@Override is a compile-time safety check. It tells the compiler 'this method is meant to override one from a parent class or interface.' If it doesn't — for example you misspell the name or get the parameters wrong — you get a compile error instead of a silent bug where your method is never called.
What is the difference between @Target and @Retention?
@Target controls WHERE an annotation may be applied (METHOD, TYPE, FIELD, PARAMETER, and so on). @Retention controls HOW LONG it survives (SOURCE = discarded after compile, CLASS = kept in the bytecode but not at runtime, RUNTIME = readable via reflection). Use the wrong @Target and the code won't compile; use the wrong @Retention and reflection silently can't find it.
Do I need to give annotation elements default values?
No, but it is good practice. An element without a default is mandatory — every use of the annotation must supply it, or the code won't compile. Adding default makes the element optional, so callers can omit it. Use String name() default "unnamed"; to make name optional with a fallback.