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 Under the module system, |
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 whyT.classneeds aClasstoken passed in. -
Records and Sealed Classes — the compile-time alternative to reflectively picking a type apart.
-
Exceptions —
ReflectiveOperationExceptionand unwrappingInvocationTargetExceptionwithgetCause().