Course topics

By WebNest Studio Engineering Team

Advanced Java Tutorial

Java Annotations

An annotation is metadata attached to code — a class, method, field, or parameter — that does not change the program's logic directly but can be read by the compiler, by tools, or by other code at runtime through reflection. Annotations look like @Something placed above a declaration, and Java ships several built-in ones while also letting you define your own.

Annotations are central to how modern Java frameworks work: Spring uses them to wire dependencies and map HTTP routes, JUnit uses them to mark test methods, and JPA/Hibernate uses them to map classes to database tables. Even without a framework, the built-in annotations improve everyday code safety and clarity.

Why it matters

Modern Java frameworks are driven by annotations: @Override, @Test, @Autowired, @Entity, @GetMapping. An annotation is only metadata; something else has to read it and act. Writing a small annotation and reading it yourself shows exactly how that works.

Built-in Annotations

The most commonly used compiler-recognized annotations ship with the language itself.

  • @Override — tells the compiler this method must override a superclass/interface method; catches typos in method signatures at compile time.
  • @Deprecated — marks a method or class as discouraged for future use, producing a compiler warning at call sites.
  • @SuppressWarnings("...") — tells the compiler to suppress a specific category of warning (e.g. "unchecked") for the annotated element.
  • @FunctionalInterface — documents that an interface is intended to have exactly one abstract method, and causes a compile error if that constraint is violated.

Meta-Annotations: Retention and Target

Meta-annotations are annotations that apply to other annotations, controlling how they behave. @Retention decides how long an annotation is kept: SOURCE (discarded by the compiler, useful for tools like Lombok), CLASS (kept in the .class file but not visible at runtime — the default), or RUNTIME (kept and readable via reflection while the program runs). @Target restricts which kinds of declarations the annotation may be placed on, such as ElementType.METHOD, TYPE, or FIELD. An annotation meant to be read reflectively at runtime, as frameworks do, must be declared with @Retention(RetentionPolicy.RUNTIME).

Defining a Custom Annotation

You declare a custom annotation with @interface, optionally giving it elements (which look like methods with no body, and can have default values). Combined with reflection, a custom annotation lets you mark code with metadata and then write generic code that discovers and acts on those marks — exactly how test runners find @Test methods without you ever registering them manually.

Examples

Built-in annotations in everyday use

Java
public class AnnotationBasicsDemo {

    @FunctionalInterface
    interface Greeter {
        String greet(String name);
    }

    static class Base {
        void show() {
            System.out.println("Base.show()");
        }
    }

    static class Derived extends Base {
        @Override
        void show() {
            System.out.println("Derived.show()");
        }

        @Deprecated
        void oldMethod() {
            System.out.println("This method should no longer be used.");
        }
    }

    @SuppressWarnings("unchecked")
    static void rawTypeExample() {
        java.util.List list = new java.util.ArrayList();
        list.add("no compiler warning shown here");
        System.out.println(list.get(0));
    }

    public static void main(String[] args) {
        Base obj = new Derived();
        obj.show();

        Greeter greeter = name -> "Hello, " + name + "!";
        System.out.println(greeter.greet("Webnest"));

        rawTypeExample();
    }
}
Output
Derived.show()
Hello, Webnest!
no compiler warning shown here

A custom runtime annotation discovered through reflection

Java
import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Testable {
    String author() default "unknown";
}

class Calculator {
    @Testable(author = "Priya")
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }
}

public class CustomAnnotationDemo {
    public static void main(String[] args) {
        for (Method method : Calculator.class.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Testable.class)) {
                Testable info = method.getAnnotation(Testable.class);
                System.out.println(method.getName() + " is testable, written by " + info.author());
            }
        }
    }
}
Output
add is testable, written by Priya

Common Mistakes

  • Defining a custom annotation without @Retention(RetentionPolicy.RUNTIME) and then being confused when reflection cannot find it at runtime.
  • Forgetting @Override on an intended override, so a typo in the method signature silently creates a brand-new overload instead of failing to compile.
  • Overusing @SuppressWarnings on large blocks of code instead of the smallest possible scope, hiding warnings that mattered.
  • Assuming annotations execute code by themselves — an annotation is inert metadata until something (the compiler or reflective code) actually reads and acts on it.

Exercise

The annotation @Important has an integer element level, and one method of Billing carries it. Complete the declaration so that it is usable on methods and readable at runtime. Then, using reflection, find the annotated method and print its name and level.

Expected output: pay 2

Java · starter code
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

// TODO: keep @Important at runtime and allow it on methods only
@interface Important {
    int level();
}

class Billing {
    @Important(level = 2)
    public void pay() { }

    public void log() { }
}

public class FindImportant {
    public static void main(String[] args) {
        for (Method method : Billing.class.getDeclaredMethods()) {
            // TODO: if the method has @Important, print its name and the level
        }
    }
}
Show hints
  • An annotation is declared with @interface, and needs @Retention(RetentionPolicy.RUNTIME) to be visible to reflection.
  • method.getAnnotation(Important.class) returns the annotation, or null when the method does not have it.
Show solution
Java · solution
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Important {
    int level();
}

class Billing {
    @Important(level = 2)
    public void pay() { }

    public void log() { }
}

public class FindImportant {
    public static void main(String[] args) {
        for (Method method : Billing.class.getDeclaredMethods()) {
            Important important = method.getAnnotation(Important.class);
            if (important != null) {
                System.out.println(method.getName() + " " + important.level()); // pay 2
            }
        }
    }
}

Quick quiz

1. Which retention policy is needed for an annotation to be read by reflection while the program runs?

  • A. SOURCE
  • B. RUNTIME
  • C. CLASS
  • D. COMPILE
Show answer

B. SOURCE annotations are discarded by the compiler, and CLASS is the default, kept in the class file but not available to reflection.

2. What does @Override do?

  • A. Makes the method run faster
  • B. Makes the method public
  • C. Asks the compiler to report an error if the method does not actually override one in a supertype
  • D. Marks the method as deprecated
Show answer

C. It catches a misspelled name or a wrong parameter list.

3. What does @Target(ElementType.METHOD) specify?

  • A. Where the annotation may be applied: here, only to methods
  • B. How long the annotation is kept
  • C. The default value
  • D. The class that reads the annotation
Show answer

A. Using it on a class or field would be a compile-time error.

Interview questions

What is an annotation and does it change how code runs?

Show answer

An annotation is metadata attached to a class, method, field or parameter. By itself it does nothing: it has no logic. It takes effect only when something reads it: the compiler, as with @Override; an annotation processor at build time, as with Lombok; or a framework at runtime through reflection, as with @Autowired or @Test.

What are meta-annotations?

Show answer

They are annotations applied to annotation declarations. @Retention states how long the annotation is kept: source only, class file, or runtime. @Target states which program elements it may be placed on. @Documented includes it in generated documentation, @Inherited lets subclasses inherit a class-level annotation, and @Repeatable allows it to be applied more than once to the same element.

Key Points to Remember

  • @Override, @Deprecated, @SuppressWarnings, and @FunctionalInterface are the most common built-in annotations.
  • @Retention controls how long an annotation survives (SOURCE, CLASS, RUNTIME); @Target restricts where it can be applied.
  • Custom annotations are declared with @interface and become useful once reflective code discovers and acts on them.
  • An annotation by itself does nothing — its value comes entirely from the compiler checks or reflective code that reads it.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering