JAVA / PLATFORM, BUILDS AND TESTING
Reflection basics and why frameworks reach for it
Inspect and call a class's constructors, fields and methods at runtime with java.lang.reflect, and see why containers and test runners depend on it.
What you will learn
- Get a Class object from a class literal, an instance's getClass(), or Class.forName
- Choose getDeclaredX for private members declared here, getX for public inherited ones
- Unwrap InvocationTargetException to reach the error the invoked method really threw
- Cache Method and Field lookups: the name search costs far more than the invoke
Understanding Reflection basics and why frameworks reach for it
Every class file carries a complete description of itself: the names of its fields and methods, their type descriptors, and their modifiers. The JVM has to keep that metadata around to link and verify code, and reflection simply hands it to you as ordinary Java objects. A Class object is therefore a mirror of a loaded type, and Field, Method and Constructor are handles onto its individual members. The trade is where names get resolved: normal code has the compiler resolve a symbol once at compile time, while reflection resolves a string when the line runs.
That deferral is exactly what a framework needs. A dependency injection container, an object-relational mapper, a JSON library or a test runner was compiled long before your OrderRepository or your shouldRoundTrip method existed, so it cannot write new OrderRepository() or call your method by name. It receives your type as a Class object or a class name and interrogates the metadata: which methods are annotated, which fields match a column, which constructor takes no arguments. Then it builds instances with Constructor.newInstance and calls behaviour with Method.invoke. Some frameworks later generate bytecode for speed, but the discovery phase that decides what to generate is still reflection.
The cost shows up in three places. Lookups do a string search plus an access check, so frameworks cache the returned handles; invoke itself boxes arguments into an Object[] and boxes the return value, which is why hot paths graduate to MethodHandles or generated code. Access is checked at runtime against the caller, so touching a private member needs setAccessible(true), and since strong encapsulation that call fails with InaccessibleObjectException when the owning module has not opened the package. Most importantly, reflection opts out of the compiler's guarantees: rename a field and nothing breaks until the line executes, which is why good frameworks validate your classes at startup rather than mid-request.
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int sum() {
return x + y;
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}
public class ReflectionBasics {
public static void main(String[] args) throws Exception {
Class<?> type = Class.forName("Point");
System.out.println("loaded " + type.getName());
Constructor<?> ctor = type.getDeclaredConstructor(int.class, int.class);
Object obj = ctor.newInstance(3, 4);
System.out.println("instance " + obj);
Field[] fields = type.getDeclaredFields();
Arrays.sort(fields, Comparator.comparing(Field::getName));
for (Field f : fields) {
f.setAccessible(true);
System.out.println(" " + f.getType().getName() + " " + f.getName() + " = " + f.get(obj));
}
Field y = type.getDeclaredField("y");
y.setAccessible(true);
y.setInt(obj, 40);
Method sum = type.getMethod("sum");
System.out.println("sum() -> " + sum.invoke(obj));
System.out.println("now " + obj);
}
}
Reflection exposes the metadata a class file already carries about itself, so code compiled before a type existed can still create, read and call it by name.
Worked examples
Invoking a method and finding the real exception
Shows that a failure inside the invoked method arrives wrapped, so the cause must be unwrapped.
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class InvokeAndUnwrap {
static class Service {
public String greet(String name) {
return "hi " + name;
}
public void boom() {
throw new IllegalStateException("not ready");
}
}
public static void main(String[] args) throws Exception {
Service s = new Service();
Method greet = Service.class.getMethod("greet", String.class);
System.out.println(greet.invoke(s, "ada"));
Method boom = Service.class.getMethod("boom");
try {
boom.invoke(s);
} catch (InvocationTargetException e) {
System.out.println("wrapper: " + e.getClass().getSimpleName());
System.out.println("cause: " + e.getCause());
}
}
}
Example explained
Line 1getMethod("greet", String.class) matches on name plus exact parameter types, because that pair is what identifies a method in the class file.
Line 2invoke returns Object, so the String result comes back as a reference and is printed through println(Object).
Line 3Anything thrown inside boom() is caught by the reflection machinery and rethrown wrapped, so catching IllegalStateException here would never fire.
Line 4The wrapper has no message of its own; getCause() gives you the original exception with its original message and stack trace.
Wiring an implementation chosen by name
Reproduces the core of what a container does when a config file names a class it was never compiled against.
import java.lang.reflect.Constructor;
interface Greeter {
String greet();
}
class Formal implements Greeter {
public String greet() {
return "Good evening.";
}
}
class Casual implements Greeter {
public String greet() {
return "Yo.";
}
}
public class LoadByName {
static Greeter load(String className) throws Exception {
Class<?> c = Class.forName(className);
if (!Greeter.class.isAssignableFrom(c)) {
throw new IllegalArgumentException(className + " is not a Greeter");
}
Constructor<?> ctor = c.getDeclaredConstructor();
return (Greeter) ctor.newInstance();
}
public static void main(String[] args) throws Exception {
System.out.println(load("Formal").greet());
System.out.println(load("Casual").greet());
try {
load("java.lang.String");
} catch (IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
try {
load("Polite");
} catch (ClassNotFoundException e) {
System.out.println("missing: " + e.getMessage());
}
}
}
Example explained
Line 1Class.forName asks the class loader to resolve a name that exists only as a string, which is how a property value or annotation attribute becomes a live type.
Line 2isAssignableFrom performs at runtime the compatibility check the compiler would normally do, so the cast on the next lines cannot fail.
Line 3getDeclaredConstructor() with no arguments finds the no-arg constructor even though it is package-private, and newInstance() runs it.
Line 4A misspelled name surfaces as ClassNotFoundException whose message is just the name that could not be found, showing where compile-time safety ends.
Important notes
setAccessible(true) is not a master key. On a member of a package that its module has not opened it throws InaccessibleObjectException, which is why frameworks ask for an opens directive or --add-opens.
getDeclaredFields returns members in unspecified order and includes compiler-generated ones such as this$0 in inner classes or $VALUES in enums, so sort the array and filter with isSynthetic() when the output must be stable.
Common mistakes
Using getMethod for a private or package-private member: it only returns public members, so you get NoSuchMethodException for a method plainly visible in the source. Use getDeclaredMethod on the declaring class, and walk getSuperclass() yourself for inherited non-public members.
Describing a primitive parameter with its wrapper, as in getMethod("shift", Integer.class) for shift(int): the descriptor recorded in the class file is int, so the lookup fails with NoSuchMethodException even though invoke would happily accept a boxed Integer as the argument value.
Catching Exception around invoke and logging e.getMessage(): InvocationTargetException's own message is null, so the log says null and the real IllegalStateException disappears.
Try it yourself
Change, predict, then run
Write a static void dump(Object o) that prints the declaring class name and then every non-synthetic declared field as name=value, using getClass() and setAccessible, and call it with two unrelated small classes to confirm one method handles both.
Open the Java workspaceCheck your understanding
A JSON library must create a Point instance before it can fill in x and y. Why do such libraries usually insist on a no-argument constructor?
- The library learns field names from the class metadata, but parameter names are normally absent from the class file, so it cannot tell which constructor argument matches which JSON key
- Constructor.newInstance is unable to pass arguments to a constructor
- setAccessible only works on objects that were created by a no-argument constructor
- Reflection cannot see private or package-private constructors at all
Show answer
Field names survive in the class file, so the library can match a JSON key to a field, but constructor parameter names are dropped unless the class was compiled with -parameters; a no-arg constructor sidesteps that matching problem entirely. Option 2 is the tempting one and is simply false: newInstance takes varargs and unboxes them, as the main example does with newInstance(3, 4). getDeclaredConstructor plus setAccessible reaches private constructors, and setAccessible knows nothing about how an object was built.