Java Reflection API: Inspect & Manipulate Code at Runtime
A practical deep dive into one of Java's most powerful — and misunderstood — features
Tags: #java #reflection #jvm #tutorial
Table of Contents
- What is Reflection?
- Getting a Class Object
- Inspecting Fields, Methods & Constructors
- Invoking Methods Dynamically
- Accessing Private Members
- Annotations via Reflection
- Real-World Use Cases
- Performance & Pitfalls
1. What is Reflection?
Java Reflection is the ability of a program to inspect and modify its own structure and behaviour at runtime. Through the java.lang.reflect package, you can examine classes, interfaces, fields, methods, and constructors — even if you don't know their names at compile time.
Think of it as a mirror held up to your own code: you can look at what exists, read its metadata, and call it dynamically.
💡 When does it shine? Frameworks like Spring, Hibernate, and JUnit rely heavily on Reflection to wire dependencies, map entities, and discover test methods — all without you writing explicit calls.
2. Getting a Class Object
Every Reflection operation starts with a Class<?> object. There are three main ways to get one:
// 1. From the class literal (compile-time known type)
Class<String> c1 = String.class;
// 2. From an instance at runtime
String s = "hello";
Class<?> c2 = s.getClass();
// 3. By fully-qualified name (most dynamic)
Class<?> c3 = Class.forName("com.example.MyService");
Class.forName() is the most powerful form — it lets you load classes whose names are only known at runtime (e.g. read from a config file or database).
3. Inspecting Fields, Methods & Constructors
Fields
Class<?> clazz = Person.class;
// Only public fields (includes inherited)
Field[] publicFields = clazz.getFields();
// All declared fields (private, protected, package) — no inherited
Field[] allFields = clazz.getDeclaredFields();
for (Field f : allFields) {
System.out.println(f.getName() + " → " + f.getType().getSimpleName());
}
Methods
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
System.out.println(
m.getName()
+ " | params: " + m.getParameterCount()
+ " | returns: " + m.getReturnType().getSimpleName()
);
}
Constructors
// Instantiate with a specific constructor
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, int.class);
Object instance = ctor.newInstance("Alice", 30);
4. Invoking Methods Dynamically
Once you have a Method object, call invoke() with the target instance and arguments. This is the core of dynamic dispatch.
public class Calculator {
public int add(int a, int b) { return a + b; }
}
// --- Reflection call ---
Calculator calc = new Calculator();
Method addMethod = Calculator.class.getMethod("add", int.class, int.class);
Object result = addMethod.invoke(calc, 3, 7);
System.out.println(result); // 10
💡 Tip:
getMethod()searches public methods including inherited ones. UsegetDeclaredMethod()to target a specific class's own methods regardless of visibility.
5. Accessing Private Members
Reflection can bypass Java's access control — a double-edged sword. You must call setAccessible(true) before reading or writing a private member.
public class Secret {
private String password = "hunter2";
}
Secret obj = new Secret();
Field f = Secret.class.getDeclaredField("password");
f.setAccessible(true);
System.out.println(f.get(obj)); // hunter2
f.set(obj, "newpass"); // mutation!
⚠️ Warning: In Java 16+ (strong encapsulation via the Module System),
setAccessible()on JDK internal classes may throwInaccessibleObjectException. Always handle this with a try-catch and prefer proper APIs when available.
6. Annotations via Reflection
Reflection is the standard way to read custom annotations at runtime. This is the backbone of frameworks that use annotations as metadata (e.g. @Autowired, @Entity, @Test).
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
String level() default "INFO";
}
public class Service {
@Log(level = "DEBUG")
public void processOrder() { /* ... */ }
}
// Reading annotation at runtime
Method m = Service.class.getMethod("processOrder");
if (m.isAnnotationPresent(Log.class)) {
Log log = m.getAnnotation(Log.class);
System.out.println("Log level: " + log.level()); // DEBUG
}
7. Real-World Use Cases
| Use Case | Example | How Reflection Helps |
|---|---|---|
| Dependency Injection | Spring @Autowired
|
Scans beans and injects dependencies without manual wiring |
| ORM Mapping | Hibernate @Entity
|
Maps Java fields to DB columns using annotations |
| Serialization | Jackson ObjectMapper | Reads fields/getters to convert objects to JSON automatically |
| Unit Testing | JUnit @Test
|
Discovers and invokes test methods dynamically |
| Plugin Systems | Custom CLIs / IDEs | Loads and runs classes from external JARs at runtime |
8. Performance & Pitfalls
Performance cost
Reflective calls are significantly slower than direct calls — typically 10–100x for cold invocations. The JIT compiler cannot inline or optimize them as effectively. Always cache Method, Field, and Constructor objects rather than looking them up on every call.
// ❌ Slow: lookup inside a hot loop
for (Object o : items) {
Method m = o.getClass().getMethod("process"); // repeated lookup!
m.invoke(o);
}
// ✅ Fast: cache the Method once
Method m = MyClass.class.getMethod("process");
for (Object o : items) {
m.invoke(o); // cached — much cheaper
}
Key pitfalls to avoid
-
Breaking encapsulation — calling
setAccessible(true)carelessly can violate the module system in Java 16+ -
Swallowed exceptions —
invoke()wraps target exceptions inInvocationTargetException; always unwrap and handle them - Overusing reflection — prefer type-safe, compile-time alternatives when the type is known
- AOT incompatibility — reflection can hinder GraalVM Native Image compilation; register reflective access explicitly if needed
💡 Modern alternative: For high-performance dynamic dispatch, consider
MethodHandle(Java 7+) orVarHandle(Java 9+). They are faster than classic Reflection and integrate better with the JIT.
Wrapping Up
Java Reflection is an incredibly powerful tool — it powers the frameworks you use every day. The key is to use it intentionally: reach for it when you genuinely need runtime flexibility, cache your reflective objects, and always handle the checked exceptions it throws. When performance is critical, MethodHandle is your friend.
Now go build something meta. Happy coding! 🚀
If you found this helpful, drop a ❤️ and feel free to share!
Top comments (0)