Lesson 31 • Advanced
Reflection API
Inspect and control classes, methods, and fields while your program is running. By the end you'll read fields, invoke methods by name, build objects without new, and understand why this powers Spring, Hibernate, and JUnit — and when to leave it alone.
What You'll Learn in This Lesson
- ✓Get a Class<?> object via getClass(), .class, and Class.forName()
- ✓Inspect fields, methods, and constructors at runtime
- ✓Read and write private members with setAccessible(true)
- ✓Invoke methods dynamically by their string name
- ✓Create instances without the new keyword
- ✓Read annotations at runtime — and when NOT to use reflection
Before You Start
This is an advanced topic. You should be comfortable with classes and objects, access modifiers like private, and the basics of annotations. Reflection works at the JVM level, deliberately bypassing the compile-time checks those features rely on — so understanding the normal rules first makes it clear what reflection is breaking.
Real-World Analogy: Opening the Remote
💡 Analogy: Normally you use an object like a TV remote — you press the labelled buttons and never think about the circuit board inside. Reflection is prising the case open. Now you can see every chip and wire, read values the buttons never exposed, and even solder new connections at runtime. It's powerful, but you've voided the warranty: the manufacturer's safety checks no longer protect you, and it's easy to break something.
That's the trade-off in one sentence. Code written before your class even existed can work with it — that's how Spring creates your beans, Hibernate maps your entities, and JUnit finds your @Test methods. But you give up the compiler's protection, you pay a speed cost, and you reach past private walls that were there for a reason.
1️⃣ The Starting Point: a Class<?> Object
Every reflection task begins by getting a Class object — a runtime description of a type. The angle-bracket version Class<?> just means "a Class of some type I'm not naming". There are three ways to get one, and they all return the same object for a given type.
obj.getClass()— when you already have an object.String.class— when you know the type at compile time.Class.forName("java.lang.String")— when the type is only a String (from a config file, plugin, or user input). This is the most dynamic, and the one frameworks lean on.
Once you hold a Class object, you can ask it questions: its name, its fields, its methods, its constructors, its annotations.
public class Main {
public static void main(String[] args) throws Exception {
String text = "hello";
// Three ways to get the same Class object:
// 1) From an existing object — .getClass()
Class<?> c1 = text.getClass(); // you HAVE an object
// 2) From the type itself — .class literal
Class<?> c2 = String.class; // you KNOW the type at compile time
// 3) From a name — Class.forName(...) (most dynamic)
Class<?> c3 = Class.forName("java.lang.String"); // type is a String!
System.out.println("c1 = " + c1.getName()); // java.lang.String
System.out.println("c2 = " + c2.getName()); // java.lang.String
System.out.println("c3 = " + c3.getName()); // java.lang.String
// All three describe the SAME class, so they are identical:
System.out.println("All equal? " + (c1 == c2 && c2 == c3)); // true
// Handy questions a Class object can answer:
System.out.println("Simple name: " + c1.getSimpleName()); // String
System.out.println("Is interface? " + c1.isInterface()); // false
}
}c1 = java.lang.String
c2 = java.lang.String
c3 = java.lang.String
All equal? true
Simple name: String
Is interface? false2️⃣ Inspect, Invoke, and Instantiate
With a Class object in hand you can do four core things. The example below does all four on a small User class:
- Inspect fields:
getDeclaredFields()lists every field the class declares (includingprivateones). - Read a value:
field.get(obj)returns that field's value from a specific object. - Invoke a method by name:
clazz.getMethod("greet")thenmethod.invoke(obj)is the dynamic equivalent ofobj.greet(). - Create an instance:
constructor.newInstance(args...)builds an object with nonewkeyword.
Notice setAccessible(true) in the example. getDeclaredField / getDeclaredMethod can find private members, but the JVM still blocks access until you explicitly switch the check off. That single call is what lets the code read the secret field and call privateMethod().
import java.lang.reflect.*;
public class Main {
static class User {
public String name;
public int age;
private String secret = "hidden123";
public User(String name, int age) { this.name = name; this.age = age; }
public String greet() { return "Hi, I'm " + name; }
private String privateMethod() { return "secret: " + secret; }
}
public static void main(String[] args) throws Exception {
User user = new User("Alice", 30);
Class<?> clazz = user.getClass(); // the User class, at runtime
// 1) List the fields this class declares
System.out.println("FIELDS:");
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true); // allow reading private fields too
System.out.println(" " + f.getName()
+ " = " + f.get(user) // f.get(user) reads the value
+ " (" + f.getType().getSimpleName() + ")");
}
// 2) Invoke a public method BY NAME (string -> behaviour)
Method greet = clazz.getMethod("greet"); // look up "greet" with no args
Object result = greet.invoke(user); // same as user.greet()
System.out.println("greet() -> " + result);
// 3) Reach a PRIVATE method — encapsulation bypassed
Method priv = clazz.getDeclaredMethod("privateMethod");
priv.setAccessible(true); // turn off the access check
System.out.println("privateMethod() -> " + priv.invoke(user));
// 4) Create a NEW object with no 'new' keyword
Constructor<?> ctor = clazz.getDeclaredConstructor(String.class, int.class);
User bob = (User) ctor.newInstance("Bob", 25); // -> new User("Bob", 25)
System.out.println("new instance -> " + bob.greet());
}
}FIELDS:
name = Alice (String)
age = 30 (int)
secret = hidden123 (String)
greet() -> Hi, I'm Alice
privateMethod() -> secret: hidden123
new instance -> Hi, I'm Bob🎯 Your Turn #1: Read a Private Field
Fill in the three blanks so the program reads the private price field of a Product. You need the field's name as a String, a call to switch off the access check, and a read with the object passed in.
import java.lang.reflect.*;
public class Main {
static class Product {
public String title = "Keyboard";
private double price = 49.99; // private on purpose
}
public static void main(String[] args) throws Exception {
// 🎯 YOUR TURN — fill in the blanks marked with ___
Product p = new Product();
Class<?> clazz = p.getClass();
// 1) Look up the PRIVATE field called "price"
Field priceField = clazz.getDeclaredField(___); // 👉 the field name in "quotes"
// 2) Turn off the access check so you can read a private field
priceField.___; // 👉 setAccessible(true)
// 3) Read its value out of object p
Object value = priceField.___; // 👉 get(p)
System.out.println("price = " + value);
// ✅ Expected output: price = 49.99
}
}3️⃣ Reading Annotations at Runtime
This is reflection's killer feature. An annotation like @Test is just metadata attached to a method. If the annotation is declared @Retention(RetentionPolicy.RUNTIME), reflection can see it while the program runs and react to it.
That's the entire idea behind JUnit: loop over a class's methods, keep only the ones where m.isAnnotationPresent(Test.class) is true, and invoke each. Spring does the same with @Component; Jackson with @JsonProperty. The example below is a working 30-line test runner.
InvocationTargetException. Call e.getCause() to get the real error — that's why the runner prints getCause().getMessage().import java.lang.annotation.*;
import java.lang.reflect.*;
public class Main {
// A custom annotation, kept at RUNTIME so reflection can see it.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {}
static class MathTests {
@Test public void addsUp() { if (2 + 2 != 4) throw new AssertionError("math broke"); }
@Test public void lengthOk() { if ("hi".length() != 2) throw new AssertionError("len broke"); }
@Test public void willFail() { throw new AssertionError("this one is meant to fail"); }
public void notATest() { throw new RuntimeException("never runs"); } // no @Test
}
public static void main(String[] args) throws Exception {
Class<?> tests = MathTests.class;
Object instance = tests.getDeclaredConstructor().newInstance();
int passed = 0, failed = 0;
for (Method m : tests.getDeclaredMethods()) {
// Only run methods annotated with @Test — that's the whole trick.
if (!m.isAnnotationPresent(Test.class)) continue;
try {
m.invoke(instance);
passed++;
System.out.println("PASS " + m.getName());
} catch (InvocationTargetException e) {
// The real exception is wrapped — unwrap with getCause()
failed++;
System.out.println("FAIL " + m.getName() + ": " + e.getCause().getMessage());
}
}
System.out.println("Results: " + passed + " passed, " + failed + " failed");
}
}PASS addsUp
PASS lengthOk
FAIL willFail: this one is meant to fail
Results: 2 passed, 1 failed🎯 Your Turn #2: Invoke a Method by Name
Fill in the blanks so the program looks up the hello method (which takes one String) and calls it on object g with the argument "World".
import java.lang.reflect.*;
public class Main {
static class Greeter {
public String hello(String who) { return "Hello, " + who + "!"; }
}
public static void main(String[] args) throws Exception {
// 🎯 YOUR TURN — fill in the blanks marked with ___
Greeter g = new Greeter();
Class<?> clazz = g.getClass();
// 1) Look up the method "hello" that takes one String parameter
Method m = clazz.getMethod(___, String.class); // 👉 the method name in "quotes"
// 2) Invoke it on object g, passing "World" as the argument
Object result = m.invoke(___, ___); // 👉 the object, then "World"
System.out.println(result);
// ✅ Expected output: Hello, World!
}
}4️⃣ Core Reflection Operations at a Glance
| Goal | API | Notes |
|---|---|---|
| Get class by name | Class.forName("pkg.MyClass") | Fully-qualified name |
| List fields | getDeclaredFields() | Incl. private, not inherited |
| List methods | getDeclaredMethods() | Incl. private, not inherited |
| Read a field | field.get(obj) | May need setAccessible(true) |
| Call a method | method.invoke(obj, args) | Dispatch by string name |
| Build an object | constructor.newInstance(args) | No new keyword |
| Check annotation | m.isAnnotationPresent(X.class) | Needs RUNTIME retention |
🧩 Mini-Challenge: Call a Private Method
Now with the scaffolding removed. You're given an Account whose deposit method is private. Using only the comment outline below, reflectively call deposit(50.0) and print the new balance. Everything you need is in the worked examples above.
import java.lang.reflect.*;
public class Main {
static class Account {
private double balance = 100.0;
private void deposit(double amount) { balance += amount; }
public double getBalance() { return balance; }
}
public static void main(String[] args) throws Exception {
// 🎯 MINI-CHALLENGE: Move money without calling deposit() directly
// 1. Make an Account and get its Class object (account.getClass())
// 2. Look up the PRIVATE method "deposit" — it takes one double
// (getDeclaredMethod("deposit", double.class))
// 3. setAccessible(true), then invoke it with 50.0 on your account
// 4. Print the balance with getBalance()
//
// ✅ Expected output: Balance: 150.0
// your code here
}
}Common Errors & Pitfalls
- ❌ The performance cost: reflective calls are several times slower than direct calls (extra lookups, access checks, and argument boxing into
Object[]). Worse, never look members up inside a hot loop — cache theMethod/Fieldobjects once, and useMethodHandlefor the hottest paths. - ❌ Breaking encapsulation:
setAccessible(true)reaches pastprivate— the very wall that keeps a class's internals safe to change. Code that does this is brittle and easy to misuse; reach for public APIs first. - ❌ Fragile to refactors:
getMethod("greet")uses a String, so the compiler can't catch a typo or a rename.NoSuchMethodExceptionorNoSuchFieldExceptionappears only at runtime, often long after the rename that caused it. - ❌ Security & the module system (JPMS): on modern Java,
setAccessibleon code in another module throwsInaccessibleObjectExceptionunless that moduleopensthe package. ASecurityManagercan also veto reflective access entirely. - ❌ Swallowing the wrapped exception: when an invoked method throws, you get
InvocationTargetException, not the original. Always unwrap withe.getCause()or your real error is hidden. - ❌ Forgetting RUNTIME retention: an annotation with the default
CLASSretention is invisible to reflection. Mark it@Retention(RetentionPolicy.RUNTIME)orisAnnotationPresentsilently returns false.
Pro Tips
💡 Cache everything. Look up Method and Field objects once at startup and reuse them — the lookup is the expensive part, not the invoke.
💡 Prefer MethodHandle (java.lang.invoke, Java 7+) for performance-sensitive reflective calls — it's JIT-friendly and far closer to direct-call speed.
💡 Prefer compile-time tools like annotation processors (Lombok, MapStruct) when you can — they generate ordinary code with zero runtime reflection cost.
💡 Proxy.newProxyInstance() builds an interface implementation at runtime — the foundation of Spring AOP and dynamic mocks.
📋 Quick Reference
| Action | Syntax | Notes |
|---|---|---|
| Class from object | obj.getClass() | You have an instance |
| Class from type | String.class | Known at compile time |
| Class from name | Class.forName("pkg.X") | Most dynamic |
| Read field | f.get(obj) | setAccessible(true) if private |
| Invoke method | m.invoke(obj, args) | Returns Object |
| New instance | ctor.newInstance(args) | No new keyword |
| Check annotation | isAnnotationPresent(X.class) | Needs RUNTIME retention |
| Dynamic proxy | Proxy.newProxyInstance(...) | Runtime interface impl |
Frequently Asked Questions
What is reflection in Java?
Reflection is an API (in java.lang.reflect) that lets a running program inspect and manipulate classes, fields, methods, and constructors by name at runtime — instead of referring to them directly in code. It is how frameworks like Spring, Hibernate, and JUnit work with classes they never saw when they were compiled.
What is the difference between getClass(), .class, and Class.forName()?
All three return the same Class object. Use object.getClass() when you already have an instance, Type.class when you know the type at compile time, and Class.forName("fully.qualified.Name") when the class name only exists as a String (for example loaded from a config file or plugin).
What does setAccessible(true) do, and is it safe?
setAccessible(true) turns off Java's access check so you can read or invoke private members. It is essential for frameworks but breaks encapsulation, can be blocked by the Java module system or a SecurityManager, and may stop working if the class is refactored. Avoid it in ordinary application code — prefer public APIs.
Why is reflection slower than a normal method call?
Reflective calls do extra work every time — looking up members, checking access, boxing arguments into Object[] — and the JIT compiler cannot optimise them as aggressively as direct calls. Cache the Method and Field objects outside loops, and use MethodHandle (java.lang.invoke) for hot paths to reduce the cost.
When should I NOT use reflection?
Avoid reflection for everyday logic where interfaces, generics, or polymorphism would do the job. It is slower, bypasses compile-time type safety (typos in method names only fail at runtime), and is fragile when code is refactored. Reach for it only when you genuinely cannot know the types in advance — frameworks, serialization, plugins, and testing tools.
🎉 Lesson Complete!
You can now get a Class object three ways, inspect and read fields, invoke methods by name, build objects without new, and react to annotations at runtime — the exact machinery inside Spring, Hibernate, and JUnit. Just as importantly, you know its costs: it's slower, it breaks encapsulation, and it's fragile to refactors, so you reach for it only when you truly can't know the types in advance.
Next up: Annotations — how to create your own metadata and process it, the perfect companion to the reflection you just learned.
Sign up for free to track which lessons you've completed and get learning reminders.