Java Reflection - Complete Deep Dive
From bytecode to OS level: How reflection works under the hood
Executive Summary
What: Reflection is Java's ability to inspect and manipulate classes, methods, fields, and objects at runtime using the java.lang.reflect package.
When to use: Frameworks (Spring, Hibernate), dependency injection, dynamic proxies, serialization, annotation processing.
Key costs: Performance overhead (10-100x slower than direct calls), security implications, type safety loss.
Best for: Framework developers, developers building dynamic systems, those optimizing reflection usage.
Quick fact: Every reflection call goes through JNI to access native code, which is slow by design to prevent abuse.
Quick Navigation
- Understand basics? Start with Overview → How It Works
- Want OS-level details? Jump to OS Level Deep Dive
- Building code? See Junior Guide → Examples
- Performance issues? Check Senior Guide + Pitfalls
Overview & Core Concepts
What is Reflection?
Reflection is the ability of a Java program to examine or "introspect" upon itself - to look at its own classes, methods, fields at runtime and potentially modify them. It's like looking in a mirror to see not just your face, but your internal organs.
Real-world analogy: Without reflection, you write code like a painter with a fixed canvas. With reflection, you can examine the canvas during painting, change its dimensions, reposition elements - all while the painting is happening.
The Reflection Chain
How Reflection Works: The Journey
Step 1: Class Loading
// When you do this:
Class<?> clazz = Class.forName("com.example.User");
// Behind the scenes:
// 1. ClassLoader searches classpath
// 2. Finds User.class bytecode
// 3. Parses bytecode
// 4. Creates Class object in metaspace
// 5. Stores reference in JVM's class table
Step 2: Introspection
// Get method:
Method method = clazz.getMethod("getName", String.class);
// JVM does:
// 1. Search method table in Class object
// 2. Verify argument types match
// 3. Check access permissions
// 4. Return Method wrapper object
Step 3: Invocation
// Call the method:
Object result = method.invoke(instance, "John");
// JVM does:
// 1. Create stack frame
// 2. Push arguments
// 3. Lookup method implementation
// 4. Jump to native code
// 5. Execute method
// 6. Return result
OS Level Deep Dive - How Reflection Actually Happens
Memory Layout During Reflection
The JNI Boundary - Where Reflection Gets Expensive
// When you call via reflection:
Method method = User.class.getMethod("getName");
Object result = method.invoke(user); // JNI boundary crossed!
// JVM has to:
// 1. Stop JIT optimization (can't inline across reflection)
// 2. Create JNI frame (stack overhead)
// 3. Convert Java objects to C++ objects (marshalling)
// 4. Call C++ method with full checks
// 5. Convert result back from C++ to Java
// 6. Clean up JNI frame
// 7. Resume JIT compilation
// COMPARISON with direct call:
Object result = user.getName(); // Direct JVM call
// JVM can:
// 1. Inline the method call
// 2. Optimize the whole call chain
// 3. Access fields directly
// 4. No marshalling needed
Bytecode Level - What's Really Happening
// Your reflection code:
public static void reflectionCall() {
User user = new User();
Method method = User.class.getMethod("getName");
Object result = method.invoke(user);
}
// Compiles to bytecode:
public static void reflectionCall();
Code:
0: new #2 (class User)
3: dup
4: invokespecial #3 (User.<init>)
7: astore_0
8: ldc #2 (class User)
10: ldc_w #4 (String "getName")
13: iconst_0
14: anewarray #5 (class [Ljava/lang/Class;)
17: invokevirtual #6 (Class.getMethod)
20: astore_1
21: aload_1
22: aload_0
23: iconst_0
24: anewarray #7 (class [Ljava/lang/Object;)
27: invokevirtual #8 (Method.invoke)
30: astore_2
31: return
// Direct call bytecode:
public static void directCall();
Code:
0: new #2 (class User)
3: dup
4: invokespecial #3 (User.<init>)
7: astore_0
8: aload_0
9: invokevirtual #9 (User.getName)
12: astore_1
13: return
// Notice: Reflection uses invokevirtual on Method object
// Direct uses invokevirtual on User object (can be inlined)
JIT Compilation and Reflection
- Before JIT sees it: Reflection runs interpreted, very slow
- After 10k+ calls: JIT compiles the Method.invoke code itself
- But: Cannot inline the actual method being called
- Result: Reflection is always slower, even after JIT
CPU Level - Assembly Code Generated
// Simplified reflection call generates:
// Direct method call (fast):
mov rax, [rbx+8] ; Load 'name' field from object (8 bytes offset)
ret ; Return value in rax
; ~2-3 CPU cycles
// Reflection call (slow):
push rbp ; Save old stack frame
mov rsp, rbp ; Create new frame (JNI overhead)
call check_access ; Security check
mov rax, [class_table] ; Load class metadata
mov rax, [rax+methods] ; Load method table
mov rax, [rax+offset] ; Get method pointer
call rax ; Indirect call (CPU can't predict)
mov r12, rax ; Save result
pop rbp ; Clean up frame
ret
; ~50-200 CPU cycles (prediction miss penalty!)
System Call Level - When Reflection Touches the OS
// Reflection to access private fields:
Field field = User.class.getDeclaredField("ssn");
field.setAccessible(true);
String ssn = (String) field.get(user);
// What happens at OS level:
// 1. JVM checks security manager
// 2. SecurityManager.checkMemberAccess() called
// 3. If enabled, JVM walks stack frames
// 4. Checks class loader permissions
// 5. Reads security policy files (disk I/O!)
// 6. Finally allows access
// This can be SLOW if:
// - Security manager is enabled
// - Checking many fields
// - Disk I/O for policy files
Memory Allocation Overhead
// Every reflection call creates objects:
Method method = User.class.getMethod("getName");
// Allocates: Method object (~200 bytes)
// Annotation array (~100 bytes per annotation)
// Parameter info (~50 bytes per parameter)
// Total: 300-500 bytes per lookup!
// If called in a loop:
for (int i = 0; i < 1_000_000; i++) {
Method m = User.class.getMethod("getName"); // NEW object each time!
Object result = m.invoke(user);
}
// Allocates 300MB+ just for Method objects!
// Plus GC pressure from collecting them
// Fix: Cache the Method object
Method method = User.class.getMethod("getName"); // Once
for (int i = 0; i < 1_000_000; i++) {
Object result = method.invoke(user); // Reuse
}
// Allocates ~200 bytes total
How Junior Developers Use Reflection
Basic Reflection Pattern
import java.lang.reflect.*;
public class ReflectionBasics {
public static void main(String[] args) throws Exception {
// 1. Get Class object
Class<?> clazz = Class.forName("java.util.ArrayList");
// 2. Get constructors
Constructor<?>[] constructors = clazz.getConstructors();
for (Constructor<?> c : constructors) {
System.out.println("Constructor: " + c);
}
// 3. Get methods
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
System.out.println("Method: " + m.getName());
}
// 4. Get fields
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
System.out.println("Field: " + f.getName() + " (" + f.getType().getSimpleName() + ")");
}
}
}
Invoking Methods via Reflection
public class ReflectionInvoke {
static class Calculator {
public int add(int a, int b) {
return a + b;
}
public String greet(String name) {
return "Hello, " + name;
}
}
public static void main(String[] args) throws Exception {
Calculator calc = new Calculator();
// Get and invoke add method
Method addMethod = Calculator.class.getMethod("add", int.class, int.class);
int result = (int) addMethod.invoke(calc, 5, 3);
System.out.println("5 + 3 = " + result); // Output: 5 + 3 = 8
// Get and invoke greet method
Method greetMethod = Calculator.class.getMethod("greet", String.class);
String greeting = (String) greetMethod.invoke(calc, "World");
System.out.println(greeting); // Output: Hello, World
}
}
Working with Fields
public class FieldReflection {
static class User {
public String name;
private int age; // Private field
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) throws Exception {
User user = new User("Alice", 30);
// Access public field
Field nameField = User.class.getField("name");
String name = (String) nameField.get(user);
System.out.println("Name: " + name); // Output: Name: Alice
// Access private field (requires setAccessible)
Field ageField = User.class.getDeclaredField("age");
ageField.setAccessible(true); // Allow access to private field
int age = (int) ageField.get(user);
System.out.println("Age: " + age); // Output: Age: 30
// Modify field
nameField.set(user, "Bob");
System.out.println("Updated name: " + user.name); // Output: Updated name: Bob
}
}
Common Beginner Mistakes
// Performance nightmare:
for (int i = 0; i < 1_000_000; i++) {
Method method = User.class.getMethod("getName"); // Called 1M times!
method.invoke(user);
}
// Each getMethod() call:
// - Searches method table
// - Allocates new Method object
// - Creates annotation arrays
// Total cost: ~500 bytes * 1M = 500MB memory, tons of GC
Fix: Cache the Method object:
Method method = User.class.getMethod("getName"); // Once
for (int i = 0; i < 1_000_000; i++) {
method.invoke(user); // Reuse - much faster
}
// Will crash if method doesn't exist:
Method method = User.class.getMethod("getName"); // What if typo?
method.invoke(user); // Throws NoSuchMethodException
Fix: Handle exceptions properly:
try {
Method method = User.class.getMethod("getName");
Object result = method.invoke(user);
} catch (NoSuchMethodException e) {
System.out.println("Method not found: " + e.getMessage());
} catch (InvocationTargetException e) {
System.out.println("Method threw exception: " + e.getTargetException());
} catch (IllegalAccessException e) {
System.out.println("Cannot access method: " + e.getMessage());
}
// This won't work as expected:
List<String> list = new ArrayList<String>();
Type[] types = list.getClass().getTypeParameters();
// types.length == 0! Generics are erased at runtime!
Why: Java generics are compile-time only. At runtime, List<String> and List<Integer> are identical.
First 3 Things Every Junior Should Know
- Reflection is slow: 10-100x slower than direct calls. Use sparingly in hot paths.
- Cache reflection results: getMethod/getField are expensive. Store the result.
- Handle exceptions: Reflection throws many exceptions. Always wrap in try-catch.
How Senior Developers Use Reflection
Performance Optimization: Understanding the Cost
import java.lang.reflect.Method;
public class ReflectionBenchmark {
static class Calculator {
public int add(int a, int b) {
return a + b;
}
}
public static void main(String[] args) throws Exception {
Calculator calc = new Calculator();
Method method = Calculator.class.getMethod("add", int.class, int.class);
// Warm up JIT
for (int i = 0; i < 100_000; i++) {
calc.add(5, 3);
method.invoke(calc, 5, 3);
}
// Benchmark direct call
long start = System.nanoTime();
for (int i = 0; i < 10_000_000; i++) {
calc.add(5, 3);
}
long directTime = System.nanoTime() - start;
// Benchmark reflection
start = System.nanoTime();
for (int i = 0; i < 10_000_000; i++) {
method.invoke(calc, 5, 3);
}
long reflectionTime = System.nanoTime() - start;
System.out.println("Direct call: " + directTime + " ns");
System.out.println("Reflection: " + reflectionTime + " ns");
System.out.println("Ratio: " + (reflectionTime / (double) directTime) + "x slower");
// Typical output:
// Direct call: 50000000 ns
// Reflection: 2500000000 ns
// Ratio: 50x slower
}
}
Memory Profiling with Reflection
public class ReflectionMemoryProfile {
public static void main(String[] args) throws Exception {
System.out.println("Initial memory: " +
(Runtime.getRuntime().totalMemory() / 1024 / 1024) + " MB");
// Reflection heavy operation
for (int i = 0; i < 100_000; i++) {
// Each call allocates new Method object (~200 bytes)
Method method = String.class.getMethod("length");
method.invoke("hello");
}
System.out.println("After reflection: " +
(Runtime.getRuntime().totalMemory() / 1024 / 1024) + " MB");
// With caching:
Method method = String.class.getMethod("length");
for (int i = 0; i < 100_000; i++) {
method.invoke("hello"); // Reuse same Method object
}
System.out.println("After cached reflection: " +
(Runtime.getRuntime().totalMemory() / 1024 / 1024) + " MB");
// Output shows caching uses 20x less memory!
}
}
MethodHandle - The Fast Alternative to Reflection
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleVsReflection {
static class Calculator {
public int add(int a, int b) {
return a + b;
}
}
public static void main(String[] args) throws Throwable {
Calculator calc = new Calculator();
// Method 1: Reflection (slow, but flexible)
java.lang.reflect.Method reflectMethod =
Calculator.class.getMethod("add", int.class, int.class);
// Method 2: MethodHandle (faster, still flexible)
MethodHandle handle = MethodHandles.lookup()
.findVirtual(Calculator.class, "add",
MethodType.methodType(int.class, int.class, int.class));
// Benchmark reflection
long start = System.nanoTime();
for (int i = 0; i < 10_000_000; i++) {
reflectMethod.invoke(calc, 5, 3);
}
long reflectionTime = System.nanoTime() - start;
// Benchmark MethodHandle
start = System.nanoTime();
for (int i = 0; i < 10_000_000; i++) {
handle.invoke(calc, 5, 3);
}
long handleTime = System.nanoTime() - start;
System.out.println("Reflection: " + reflectionTime + " ns");
System.out.println("MethodHandle: " + handleTime + " ns");
System.out.println("Speedup: " + (reflectionTime / (double) handleTime) + "x faster");
// Output:
// Reflection: 2500000000 ns
// MethodHandle: 400000000 ns
// Speedup: 6.25x faster
}
}
Caching Strategy for Reflection
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
public class ReflectionCache {
private static final ConcurrentHashMap<String, Method> methodCache =
new ConcurrentHashMap<>();
public static Object invokeMethod(Object obj, String methodName,
Class<?>[] paramTypes, Object[] args) throws Exception {
String key = obj.getClass().getName() + "." + methodName;
// Use cached method if available
Method method = methodCache.computeIfAbsent(key, k -> {
try {
return obj.getClass().getMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
return method.invoke(obj, args);
}
public static void main(String[] args) throws Exception {
String str = "hello";
// First call - caches the method
System.out.println("Length 1: " + invokeMethod(str, "length", new Class<?>[0], new Object[0]));
// Subsequent calls - uses cache (fast!)
for (int i = 0; i < 1_000_000; i++) {
invokeMethod(str, "length", new Class<?>[0], new Object[0]);
}
System.out.println("Cache size: " + methodCache.size()); // 1 (only one method cached)
}
}
Bytecode Generation - Next Level Reflection
// Instead of reflection every time, generate bytecode once
// Libraries like ByteBuddy or cglib generate classes at runtime
// Reflection approach (slow in hot path):
for (int i = 0; i < 1_000_000; i++) {
Method method = User.class.getMethod("getName");
String name = (String) method.invoke(user); // Reflection each time
}
// Bytecode generation approach (fast):
// At startup, generate an optimized accessor class:
public class UserGetterGenerated {
public String getName(User user) {
return user.getName(); // Direct call, not reflection
}
}
// Then use it in hot path:
UserGetterGenerated getter = new UserGetterGenerated();
for (int i = 0; i < 1_000_000; i++) {
String name = getter.getName(user); // Fast!
}
Debugging Reflection Issues
- Add debug logging: Log method lookup and invocation details
- Check access permissions: Ensure setAccessible(true) for private members
- Verify method signatures: Parameter types must match exactly
- Monitor memory: Track Method object allocations with profiler
- Use MethodHandle: If reflection is bottleneck, try MethodHandle
import java.lang.reflect.Method;
import java.util.Arrays;
public class ReflectionDebug {
public static Object safeInvoke(Object obj, String methodName,
Class<?>[] paramTypes, Object[] args) throws Exception {
try {
System.out.println("=== Reflection Debug ===");
System.out.println("Object: " + obj.getClass().getName());
System.out.println("Method: " + methodName);
System.out.println("Params: " + Arrays.toString(paramTypes));
System.out.println("Args: " + Arrays.toString(args));
Method method = obj.getClass().getMethod(methodName, paramTypes);
System.out.println("Found method: " + method);
Object result = method.invoke(obj, args);
System.out.println("Result: " + result);
return result;
} catch (NoSuchMethodException e) {
System.out.println("ERROR: Method not found!");
System.out.println("Available methods:");
Method[] methods = obj.getClass().getMethods();
for (Method m : methods) {
if (m.getName().equals(methodName)) {
System.out.println(" " + m);
}
}
throw e;
} catch (IllegalAccessException e) {
System.out.println("ERROR: Cannot access method (private?)");
throw e;
}
}
}
How Architects Think About Reflection
Reflection in System Design
When to Use Reflection vs. Alternatives
| Scenario | Reflection | MethodHandle | Code Generation | Interfaces |
|---|---|---|---|---|
| Framework/Plugin system | ✅ Yes (flexibility) | ⚠️ Maybe | ✅ Better | ❌ No |
| Dependency injection | ✅ Yes (Spring/Guice) | ⚠️ Maybe | ✅ Dagger (compile-time) | ❌ No |
| Hot path (millions calls) | ❌ No (slow) | ⚠️ Better | ✅ Yes (fastest) | ✅ Yes (best) |
| Unknown class at compile time | ✅ Yes (only way) | ✅ Yes | ✅ Yes (more work) | ❌ No |
| Serialization/deserialization | ✅ Yes (Jackson) | ⚠️ Maybe | ⚠️ Possible | ❌ No |
| Testing (mocking) | ✅ Yes (Mockito) | ⚠️ Maybe | ❌ Too slow | ✅ Better |
Cost Analysis: CPU, Memory, Latency
| Metric | Direct Call | Reflection | MethodHandle | Code Gen |
|---|---|---|---|---|
| Latency per call | 0.005 µs | 0.25 µs (50x) | 0.04 µs (8x) | 0.005 µs |
| Memory (100k calls) | ~0 MB | 20-50 MB | ~0 MB | 1-5 MB |
| GC pressure | None | High | Low | Low |
| JIT optimization | Full | Limited | Good | Full |
| Setup cost | Compile-time | Runtime | Runtime (small) | Runtime (large) |
Architectural Patterns
Design layers and when to use reflection:
Web Layer (REST Controllers)
└─ No reflection (direct JSON serialization)
Service Layer (Business Logic)
├─ Limited reflection (dependency injection container)
├─ Cached reflection (method invocation)
└─ Avoid in hot paths
Framework Layer (Spring, Hibernate)
├─ Heavy reflection (bean discovery, annotation processing)
├─ Caches everything (compiled method handles, bytecode)
└─ OK - only happens at startup
Annotation Processors
├─ Reflection (analyze class metadata)
└─ Generate code to replace reflection
Benefits & Pros
1. Frameworks and Dependency Injection
Without reflection, Spring couldn't exist. DI containers rely on reflection to discover beans and inject dependencies.
2. Flexible Plugin Systems
Load plugins dynamically at runtime without recompilation.
3. Serialization and Deserialization
Libraries like Jackson, Gson, and Hibernate use reflection to map objects to JSON/XML/databases.
4. Testing and Mocking
Mockito and other testing frameworks use reflection to create mocks of classes and interfaces.
5. Dynamic Proxies
Create proxy objects at runtime for cross-cutting concerns (logging, timing, caching).
6. Runtime Configuration
Load and apply configurations without recompiling code.
7. Generic Programming
Write code that works with any type by inspecting class structure at runtime.
Pitfalls & Cons
Critical Performance Issues
// DON'T do this in loops:
for (Order order : orders) {
Method method = Order.class.getMethod("getTotal"); // Called N times!
BigDecimal total = (BigDecimal) method.invoke(order); // Slow!
sum.add(total);
}
// Cost: 50x slower than direct call
// With 1M orders: 50ms becomes 2500ms
// DO THIS instead:
Method method = Order.class.getMethod("getTotal"); // Once
for (Order order : orders) {
BigDecimal total = (BigDecimal) method.invoke(order); // Reuse
sum.add(total);
}
// Reduces to ~5x overhead instead of 50x
// Accessing private fields undermines design:
Field field = User.class.getDeclaredField("password");
field.setAccessible(true);
String password = (String) field.get(user); // Breaks encapsulation!
// Problems:
// 1. Bypasses security checks
// 2. Breaks internal consistency (field might be encrypted)
// 3. Makes refactoring internal fields break external code
// 4. Creates security vulnerabilities
Better: Use public getter methods instead
// No compile-time type checking:
Method method = User.class.getMethod("getName");
Object result = method.invoke(user); // result is Object, not String
String name = (String) result; // Runtime cast - may fail!
// If you get the return type wrong:
Integer age = (Integer) method.invoke(user); // ClassCastException!
// With direct calls, compiler catches this
Memory and GC Pressure
Each reflection lookup allocates new objects (Method, Constructor, Field, Annotation arrays). In heavy usage, this causes:
- Heap pressure (millions of objects)
- GC pauses (stop-the-world)
- Young generation full
Security Implications
With setAccessible(true), you can access:
- Private fields (passwords, API keys)
- Private methods (internal implementations)
- Final fields (constants)
This allows attackers to:
- Steal secrets from objects
- Modify immutable objects
- Bypass validation
Complexity and Maintenance
// Reflection code is harder to understand:
Method method = clazz.getMethod(methodName, params); // What class? What method?
Object result = method.invoke(instance, args); // What type is result?
// IDE can't help:
// - No rename refactoring
// - No "find usages"
// - No type hints
// - Hard to track dependencies
Best Practices & Guardrails
1. Cache Reflection Results
// BAD: No caching
for (int i = 0; i < 1_000_000; i++) {
Method method = Class.class.getMethod("forName", String.class);
method.invoke(null, "java.util.ArrayList");
}
// GOOD: Cached
Method method = Class.class.getMethod("forName", String.class);
for (int i = 0; i < 1_000_000; i++) {
method.invoke(null, "java.util.ArrayList");
}
2. Use MethodHandle for Performance-Critical Code
// Reflection (slower):
Method method = Calculator.class.getMethod("add", int.class, int.class);
method.invoke(calc, 5, 3);
// MethodHandle (faster):
MethodHandle handle = MethodHandles.lookup()
.findVirtual(Calculator.class, "add",
MethodType.methodType(int.class, int.class, int.class));
handle.invoke(calc, 5, 3);
3. Avoid Reflection in Hot Paths
// BAD: Reflection inside loop
for (Object obj : objects) {
Method method = obj.getClass().getMethod("process");
method.invoke(obj); // Reflection 1M times
}
// GOOD: Reflection outside loop
Method method = ExpectedClass.class.getMethod("process");
for (Object obj : objects) {
method.invoke(obj); // Reuse method
4. Handle Exceptions Properly
try {
Method method = User.class.getMethod("getName");
Object result = method.invoke(user);
} catch (NoSuchMethodException e) {
// Method doesn't exist
} catch (InvocationTargetException e) {
// Method threw an exception
Throwable cause = e.getCause();
} catch (IllegalAccessException e) {
// Cannot access method (private without setAccessible)
} catch (IllegalArgumentException e) {
// Wrong argument types
}
5. Use Annotations + Reflection for Metadata
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {
String value();
}
public class CacheHandler {
public static void processCachedMethods(Object obj) throws Exception {
Method[] methods = obj.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Cacheable.class)) {
Cacheable cacheable = method.getAnnotation(Cacheable.class);
System.out.println("Cache key: " + cacheable.value());
// Set up caching for this method
}
}
}
}
6. Profile Reflection Usage
public class ReflectionMetrics {
private static final Map<String, Long> callCounts = new ConcurrentHashMap<>();
private static final Map<String, Long> totalTimes = new ConcurrentHashMap<>();
public static Object invokeWithMetrics(Object obj, String methodName,
Class<?>[] paramTypes, Object[] args) throws Exception {
String key = obj.getClass().getSimpleName() + "." + methodName;
long start = System.nanoTime();
try {
Method method = obj.getClass().getMethod(methodName, paramTypes);
return method.invoke(obj, args);
} finally {
long elapsed = System.nanoTime() - start;
callCounts.merge(key, 1L, Long::sum);
totalTimes.merge(key, elapsed, Long::sum);
}
}
public static void printMetrics() {
callCounts.forEach((method, count) -> {
long totalTime = totalTimes.get(method);
long avgTime = totalTime / count;
System.out.printf("%s: %d calls, %d µs avg\n",
method, count, avgTime / 1000);
});
}
}
7. Document Reflection Usage
/**
* Invokes a method via reflection with caching.
*
* Performance: ~50x slower than direct call.
* Cache: Method objects are cached in methodCache.
* Thread-safe: Yes, uses ConcurrentHashMap.
*
* Example:
* User user = new User("John");
* String name = (String) ReflectionUtil.invoke(user, "getName");
*/
public static Object invoke(Object obj, String methodName, Object... args)
throws Exception {
// Implementation
}
Real-World Examples
Example 1: Building a Simple Dependency Injection Container
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class SimpleContainer {
private final Map<Class<?>, Object> singletons = new HashMap<>();
private final Map<Class<?>, Class<?>> bindings = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> void bind(Class<T> interfaceClass, Class<? extends T> implClass) {
bindings.put(interfaceClass, implClass);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> clazz) throws Exception {
// Check singleton cache
if (singletons.containsKey(clazz)) {
return (T) singletons.get(clazz);
}
// Get implementation class
Class<?> implClass = bindings.getOrDefault(clazz, clazz);
// Create instance using reflection
Constructor<?> constructor = implClass.getConstructor();
Object instance = constructor.newInstance();
// Cache singleton
singletons.put(clazz, instance);
return (T) instance;
}
// Usage:
public static void main(String[] args) throws Exception {
SimpleContainer container = new SimpleContainer();
// Bind interface to implementation
container.bind(UserRepository.class, UserRepositoryImpl.class);
// Get instance (creates via reflection)
UserRepository repo = container.get(UserRepository.class);
System.out.println(repo.findUser(1));
}
interface UserRepository {
String findUser(int id);
}
static class UserRepositoryImpl implements UserRepository {
@Override
public String findUser(int id) {
return "User " + id;
}
}
}
Example 2: Generic Object Cloner Using Reflection
import java.lang.reflect.Field;
import java.util.Arrays;
public class DeepCloner {
@SuppressWarnings("unchecked")
public static <T> T deepClone(T obj) throws Exception {
if (obj == null) return null;
Class<T> clazz = (Class<T>) obj.getClass();
// Create new instance (calls default constructor)
T clone = clazz.newInstance();
// Copy all fields
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null) {
field.set(clone, null);
} else if (value instanceof String || value instanceof Number ||
value instanceof Boolean) {
// Immutable types - can be copied directly
field.set(clone, value);
} else {
// Recursive clone for other objects
field.set(clone, deepClone(value));
}
}
return clone;
}
// Usage:
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" + "name='" + name + "', age=" + age + "}";
}
}
public static void main(String[] args) throws Exception {
User original = new User("Alice", 30);
User cloned = deepClone(original);
System.out.println("Original: " + original);
System.out.println("Cloned: " + cloned);
System.out.println("Same object? " + (original == cloned)); // false
}
}
Example 3: Configuration Loader with Reflection
import java.lang.reflect.Field;
import java.util.Map;
public @interface Config {
String value();
String defaultValue() default "";
}
public class ConfigLoader {
public static void loadConfig(Object obj, Map<String, String> configMap)
throws Exception {
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (!field.isAnnotationPresent(Config.class)) {
continue;
}
Config config = field.getAnnotation(Config.class);
String key = config.value();
// Get value from config map, or use default
String value = configMap.getOrDefault(key, config.defaultValue());
if (value.isEmpty()) {
throw new RuntimeException("Missing config: " + key);
}
// Convert to field type and set
field.setAccessible(true);
Object convertedValue = convert(value, field.getType());
field.set(obj, convertedValue);
}
}
private static Object convert(String value, Class<?> type) {
if (type == String.class) return value;
if (type == int.class) return Integer.parseInt(value);
if (type == long.class) return Long.parseLong(value);
if (type == boolean.class) return Boolean.parseBoolean(value);
throw new IllegalArgumentException("Unsupported type: " + type);
}
// Usage:
static class DatabaseConfig {
@Config("db.url")
String url;
@Config("db.port")
int port;
@Config("db.pool-size")
int poolSize = 10;
@Override
public String toString() {
return "DatabaseConfig{" +
"url='" + url + "', port=" + port + ", poolSize=" + poolSize + "}";
}
}
public static void main(String[] args) throws Exception {
Map<String, String> config = Map.of(
"db.url", "localhost",
"db.port", "5432"
// poolSize uses default value
);
DatabaseConfig dbConfig = new DatabaseConfig();
loadConfig(dbConfig, config);
System.out.println(dbConfig);
}
}
Learning Resources
Official Documentation
- Java Reflection API: https://docs.oracle.com/javase/tutorial/reflect/
- MethodHandle (java.lang.invoke): https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/MethodHandle.html
- Bytecode Specification: https://docs.oracle.com/javase/specs/jvms/se17/html/
Related Topics to Study
- Bytecode Engineering: ASM, ByteBuddy libraries for runtime class modification
- Annotation Processing: Process annotations at compile-time instead of runtime
- Dynamic Proxies: Create proxies without reflection for logging, caching
- JVM Internals: Metaspace, class loading, JIT compilation
- Performance Profiling: JProfiler, YourKit for measuring reflection impact
Key Takeaways
- ✅ Use for: Frameworks, plugin systems, serialization, testing
- ❌ Avoid for: Hot loops, performance-critical code, security-sensitive operations
- 🚀 Optimize: Cache results, use MethodHandle, profile usage
- 🔒 Secure: Never use setAccessible for untrusted input, validate method signatures