Reflection and Annotations — Junior¶
What? Reflection is the JDK's ability to inspect and call code by name at runtime — load a class given its string name, list its methods and fields, read or write fields, and invoke methods you didn't reference at compile time. Annotations are metadata you attach to declarations (
@Override,@Deprecated, your own@interface) that the compiler, the JVM, or framework code can read. How? Reflection lives injava.lang.Classand thejava.lang.reflectpackage:Class<?>,Method,Field,Constructor,Parameter. You start from aClass<?>object (MyClass.classorClass.forName("a.b.MyClass")), ask it for aMethod, andinvokeit. Annotations are declared with@interface, retained at one of three levels (SOURCE,CLASS,RUNTIME), and read off aClass/Method/FieldwithgetAnnotation(...).
1. What reflection actually is¶
Java is statically typed and statically linked: at compile time, every method call resolves to an exact target by name and signature. Reflection is the escape hatch that lets your program ask, at runtime, "what types and methods exist in this class loaded right now?" and call them indirectly.
Every loaded class in the JVM has a corresponding Class<?> object — a piece of metadata describing its name, methods, fields, supertypes, and annotations. Reflection is the API that reads (and to a limited degree, writes) that metadata.
You will not write reflection in everyday code. You will read it constantly — almost every Java framework you touch (Spring, Hibernate, Jackson, JUnit, Mockito) is reflection underneath. Knowing the API stops these frameworks from feeling like magic.
2. The first reflective call¶
The classic three-step reflective invocation: load the class, look up the method, call it.
import java.lang.reflect.Method;
public class FirstReflection {
public static String greet(String name) {
return "Hello, " + name;
}
public static void main(String[] args) throws Exception {
// 1. Load the class object by fully qualified name.
Class<?> clazz = Class.forName("FirstReflection");
// 2. Look up the method by name and parameter types.
Method m = clazz.getMethod("greet", String.class);
// 3. Invoke it. First argument is the receiver (null for static); rest are parameters.
String result = (String) m.invoke(null, "world");
System.out.println(result); // Hello, world
}
}
Three things to notice:
Class.forName(...)takes the fully qualified class name as a string. The JVM finds it through the same class loader that loaded the caller.getMethod(name, paramTypes...)finds a public method whose declared parameter types match. For non-public methods you needgetDeclaredMethod(...).invoke(receiver, args...)calls the method. For static methods, the receiver isnull. The return type isObject— you cast it back.
3. Fields, constructors, modifiers¶
The same pattern works for fields and constructors. Each one has a dedicated reflection type.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class Person {
public String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class InspectPerson {
public static void main(String[] args) throws Exception {
Class<?> c = Person.class;
// Construct a Person reflectively.
Constructor<?> ctor = c.getConstructor(String.class, int.class);
Object p = ctor.newInstance("Ada", 36);
// Read a public field.
Field name = c.getField("name");
System.out.println(name.get(p)); // Ada
// List all declared fields (public + non-public, this class only).
for (Field f : c.getDeclaredFields()) {
System.out.println(Modifier.toString(f.getModifiers())
+ " " + f.getType().getSimpleName()
+ " " + f.getName());
}
// prints:
// public java.lang.String name
// private int age
}
}
Modifier is a small utility class with constants and toString helpers — useful for printing readable modifier strings without bit-twiddling int flags.
4. setAccessible(true) and why private fields aren't really private under reflection¶
Java enforces access control at compile time and, to a degree, at runtime. Reflection bypasses it — if you ask it to. The escape hatch is setAccessible(true):
Field age = Person.class.getDeclaredField("age");
// age.get(p); // throws IllegalAccessException — age is private
age.setAccessible(true); // disable the access check
System.out.println(age.get(p)); // 36
This is how serialisation libraries read private fields, how testing frameworks invoke private helpers, and how Spring assigns dependencies into non-public fields.
Two important catches:
- Calling
setAccessible(true)is not silent permission. Since Java 9 the module system (JPMS) blocks reflective access into modules that have not declaredopensfor the package —setAccessiblewill throwInaccessibleObjectException. See ../02-jpms-modules/ for the module rules. - It is a security-sensitive operation. A
SecurityManager(pre-Java 17) could veto it; in modern Java it is restricted by module boundaries instead.
For a first pass: if your reflective code worked on Java 8 and stopped working on Java 9+, the answer is almost always "you crossed a module boundary, add --add-opens."
5. What an annotation actually is¶
An annotation is metadata you attach to a declaration. @Override, @Deprecated, @SuppressWarnings are the three you have already seen. They look like keywords but they are types — instances of an @interface declaration.
public class Animal {
public void speak() { }
}
public class Dog extends Animal {
@Override // built-in compiler annotation
public void speak() { System.out.println("woof"); }
@Deprecated(since = "2.0", forRemoval = true)
public void bark() { speak(); }
}
@Overrideis a compiler check: javac refuses to compile ifspeak()doesn't actually override anything. It's a safety net against typos and signature drift.@Deprecatedis read at compile time (warning) and at runtime (visible via reflection); IDEs strike it through.
Built-in annotations you should recognise without thinking:
| Annotation | Purpose |
|---|---|
@Override | "This overrides a supertype method." Compiler checks. |
@Deprecated | "Don't use this in new code." Compiler warns; JDK 9+ adds forRemoval. |
@SuppressWarnings | "Stop warning me about X for this scope." e.g. "unchecked". |
@SafeVarargs | "I know what I'm doing with this varargs of a generic type." |
@FunctionalInterface | "This interface has exactly one abstract method." Compiler enforces. |
6. Declaring your own annotation¶
The keyword is @interface. The body lists elements — each one looks like a method without a body, optionally with a default.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // visible at runtime via reflection
@Target(ElementType.METHOD) // legal on methods only
public @interface Timed {
String value() default ""; // optional label
long warnAboveMillis() default 1000;
}
Use it:
public class ReportJob {
@Timed(value = "monthly-report", warnAboveMillis = 5000)
public void run() { /* ... */ }
}
Three things every custom annotation declares:
- Retention — until when does this annotation survive?
SOURCE(discarded by javac),CLASS(kept in.classbut not loaded by the runtime),RUNTIME(visible to reflection). Default isCLASS. If you want to read it at runtime, you needRUNTIME. - Target — which declarations may this annotation appear on?
METHOD,FIELD,TYPE,PARAMETER,CONSTRUCTOR, etc. Without@Target, it's legal everywhere. - Elements — each "method" defines a key. Allowed types: primitives,
String,Class, enums, other annotations, and arrays of those.
7. Reading an annotation at runtime¶
If the retention is RUNTIME, you can ask any Class, Method, Field, etc. for its annotations.
import java.lang.reflect.Method;
for (Method m : ReportJob.class.getDeclaredMethods()) {
Timed t = m.getAnnotation(Timed.class);
if (t != null) {
System.out.println(m.getName() + " is @Timed, warn > " + t.warnAboveMillis() + " ms");
}
}
getAnnotation(Class) returns null if the annotation isn't there. isAnnotationPresent(Class) returns a boolean if you only want a yes/no answer.
This is how JUnit finds @Test, how Spring finds @Component, and how Jackson finds @JsonProperty. The mechanism is the same one your Timed example uses.
8. Common newcomer surprises¶
Surprise 1: Method.invoke wraps your exception. If the called method throws, invoke does not throw that exception — it throws InvocationTargetException with your exception as its cause:
try {
method.invoke(target);
} catch (InvocationTargetException wrapped) {
Throwable real = wrapped.getCause(); // <-- this is your actual exception
}
Forgetting to unwrap is the most common reflection bug. See find-bug.md for a full example.
Surprise 2: @Retention(SOURCE) annotations don't exist at runtime. If you write:
…you cannot then read it via getAnnotation(MyMarker.class) — it was deleted by javac. Lombok's @Getter is SOURCE-only; you cannot reflect it.
Surprise 3: Reflection is slow. A reflective Method.invoke is typically 10–100 times slower than a direct call (millions of calls per second instead of billions). Frameworks cache Method objects and use MethodHandle to mitigate this. Don't put invoke in a hot loop without measuring.
Surprise 4: setAccessible(true) may throw on Java 9+. If the package is not open to your module, the JPMS refuses. The fix is --add-opens module/package=ALL-UNNAMED in JVM args, or a proper module-info.java opens directive.
Surprise 5: Primitive types have Class objects too. int.class, void.class, int[].class are all real Class<?> instances — use them when looking up methods that take primitives:
9. Quick rules¶
- Three reflective steps:
Class.forName(...)(orX.class) →getMethod(...)→invoke(...). -
getMethodfinds public methods including inherited ones;getDeclaredMethodfinds this class's methods regardless of access. -
setAccessible(true)is required for private members and may be blocked by JPMS. - Custom annotations need
@Retention(RUNTIME)to be readable by reflection. -
@Targetsays where the annotation may appear;defaultvalues let elements be optional. -
Method.invokewraps thrown exceptions inInvocationTargetException— always unwrap. - Reflection is 10–100× slower than direct calls; cache
Methodobjects when reuse matters. - You're using reflection every time you use Spring, Hibernate, Jackson, or JUnit — you just don't see it.
10. What's next¶
| Topic | File |
|---|---|
| Custom annotation + processor, MethodHandle as a faster alt | middle.md |
| MethodHandle/VarHandle perf, JPMS impact, ServiceLoader | senior.md |
| Reviewing reflection in PRs, ArchUnit checks, mentoring | professional.md |
| Where reflection and annotations live in JLS/JVMS/JEPs | specification.md |
| Ten reflection bugs with the exact stack trace they produce | find-bug.md |
Reflection benchmarks, caching, LambdaMetafactory | optimize.md |
| Hands-on exercises | tasks.md |
| Interview Q&A | interview.md |
Memorize this: reflection lets you look up code by name at runtime via Class → Method/Field → invoke/get; annotations are typed metadata declared with @interface and survive at one of three retention levels. Frameworks live on these two features. You will read more reflection than you write — but when you write it, expect InvocationTargetException, IllegalAccessException, InaccessibleObjectException, and a 10× slowdown unless you cache.