Annotations

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.

Learn Annotations in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

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.

💡 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.

Java ships with a handful of annotations the compiler already understands. You'll use these four constantly:

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.

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.

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") .

A meta-annotation describes how your annotation behaves. These two are the ones you must get right:

@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.

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.

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.

🎯 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.

🧩 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.

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.

Next up: IO & NIO — reading and writing files, streams, and non-blocking I/O.

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.