Annotations and Reflection

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

An annotation is metadata attached to a declaration: it changes how the compiler, build tools, or libraries treat the code without changing what the code does at that point. Reflection is the runtime API for inspecting classes, their members, and their annotations, and for invoking them by name. This page covers the built-in annotations, how to declare your own, and how to read both with java.lang.reflect. References: the Java Tutorials Annotations lesson, dev.java: Annotations, and dev.java: Introduction to Java Reflection.

Built-in Annotations

The JDK ships a handful of annotations in java.lang that the compiler understands directly. See Predefined Annotation Types.

import java.util.List;

class Base {
    Object value() { return 0; }
}

class Derived extends Base {

    @Override                        // compile error if this does not actually override
    Object value() { return 42; }

    @Deprecated(since = "2.1", forRemoval = true)   // scheduled for deletion; callers get a warning
    void oldApi() { }

    @SuppressWarnings("unchecked")   // silence one known-safe warning, narrowly scoped
    List<String> castRaw(Object raw) {
        return (List<String>) raw;
    }

    @SafeVarargs                     // promise: this generic varargs method does not leak the array
    static <T> List<T> firstTwo(T... items) {
        return List.of(items[0], items[1]);
    }
}

@FunctionalInterface                 // compile error if a second abstract method is added
interface Parser<T> {
    T parse(String text);
}

@Override catches typos in method signatures and accidental overloads. @Deprecated takes two elements: since records the release that deprecated the member, and forRemoval = true promotes the compiler note to a stronger warning and signals that a future release will delete it. @SuppressWarnings turns off named compiler warnings ("unchecked", "deprecation", "rawtypes") for the smallest declaration you can put it on — a single field or method, never a whole class. @FunctionalInterface makes the compiler enforce "exactly one abstract method" (see Interfaces). @SafeVarargs suppresses the "possible heap pollution" warning on a static or final generic varargs method whose body only reads the array (see Generics).

$ javac -Xlint:all Derived.java
warning: [removal] oldApi() in Derived has been deprecated and marked for removal

Declaring a Custom Annotation

An annotation type is declared with @interface. Each method is an element: its return type is restricted to a primitive, String, Class, an enum, another annotation, or a one-dimensional array of those, and it may declare a default. An element named value can be supplied positionally. See Declaring an Annotation Type.

import java.lang.annotation.*;

@Documented                                   // include in generated Javadoc
@Retention(RetentionPolicy.RUNTIME)           // keep it readable by reflection
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited                                    // a subclass reports its superclass's TYPE-level @Audited
@Repeatable(Audited.List.class)               // may appear more than once on one element
public @interface Audited {

    String value();                           // required, positional: @Audited("orders")
    String level() default "INFO";            // optional, has a default
    String[] tags() default {};

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @interface List { Audited[] value(); }     // the container @Repeatable points at
}
@Audited("orders")
@Audited(value = "billing", level = "WARN", tags = {"pci", "money"})
class OrderService { }

@Retention sets how long the annotation survives: RetentionPolicy.SOURCE (discarded after compilation — @Override), CLASS (written to the class file but invisible at runtime; the default), or RUNTIME (visible to reflection). Only RUNTIME annotations can be read by the code in the next section. @Target lists the ElementType positions where the annotation is legal (TYPE, METHOD, FIELD, PARAMETER, TYPE_USE, …​); omit it and the annotation is allowed almost anywhere. @Documented keeps it in the Javadoc of anything it annotates. @Inherited makes a class annotation visible on subclasses; it has no effect on interface, method, or field annotations. @Repeatable lets the same annotation appear several times on one element, backed by a generated container annotation; see Repeating Annotations.

Reading Annotations and Members at Runtime

Reflection starts from a Class object, obtained from a class literal (OrderService.class), an instance (obj.getClass()), or a name (Class.forName("com.example.OrderService")). From there, java.lang.reflect exposes fields, methods, constructors, and the annotations on each. See the Java Tutorials Reflection trail.

import java.lang.reflect.*;

Class<?> type = Class.forName("com.example.OrderService");

// type-level annotation
Audited a = type.getAnnotation(Audited.class);
if (a != null) {
    System.out.println(a.value() + " / " + a.level());
}

// repeated annotations come back as an array
for (Audited each : type.getAnnotationsByType(Audited.class)) {
    System.out.println(each.value());
}

// every declared field, including private ones
for (Field f : type.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}

// every declared method annotated with @Audited
for (Method m : type.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Audited.class)) {
        System.out.println("audited: " + m.getName());
    }
}

To read or write a non-public member, first call setAccessible(true) on it. See Getting and Setting Field Values, Invoking Methods, and Creating New Class Instances.

import java.lang.reflect.*;

record Point(int x, int y) { }

Constructor<Point> ctor = Point.class.getDeclaredConstructor(int.class, int.class);
Point p = ctor.newInstance(3, 4);            // Point[x=3, y=4]

Field xField = Point.class.getDeclaredField("x");
xField.setAccessible(true);                  // required: the record component field is private
int x = (int) xField.get(p);                // 3

Method toString = Point.class.getMethod("toString");
String s = (String) toString.invoke(p);     // "Point[x=3, y=4]"

Reflection is the right tool for frameworks, serializers, dependency injectors, and test runners, but it is the wrong default for ordinary application code: it moves errors from compile time to run time, blocks many compiler and JIT optimisations, and bypasses private. Reach for an interface, a functional interface, or a sealed hierarchy with pattern matching first.

Under the module system, getDeclaredFields() followed by setAccessible(true) on a type in another module throws InaccessibleObjectException unless that package is declared opens in its module-info.java (or opened on the command line with --add-opens). A plain classpath application has no such restriction.

Checked failures from reflection all extend ReflectiveOperationException (ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InstantiationException). When an invoked method or constructor throws, reflection rethrows the failure wrapped in InvocationTargetException; call getCause() to recover the real exception.

See Also

  • Interfaces — @FunctionalInterface, and preferring an interface over reflective dispatch.

  • Generics — @SafeVarargs, type erasure, and why T.class needs a Class token passed in.

  • Records and Sealed Classes — the compile-time alternative to reflectively picking a type apart.

  • Exceptions — ReflectiveOperationException and unwrapping InvocationTargetException with getCause().