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

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:

AnnotationPut it onWhat it does
@OverrideA methodCompile error if it doesn't override a parent method
@DeprecatedAnythingWarns callers it's obsolete and may be removed
@SuppressWarningsAnythingSilences a named warning, e.g. ("unchecked")
@FunctionalInterfaceAn interfaceCompile 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-annotationControlsCommon values
@RetentionHow long it survivesSOURCE, CLASS (default), RUNTIME
@TargetWhere it may be usedTYPE, METHOD, FIELD, PARAMETER, ...
@DocumentedAppears in JavadocMarker (no value)
@InheritedSubclasses inherit itClass-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

Where You've Already Seen This

📋 Quick Reference

TaskSyntaxNotes
Define annotation@interface Name { }Note the @ — not plain interface
Element + defaultString 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 annotationm.getAnnotation(X.class)null if absent
Check presencem.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

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.

Related lessons