Java Reflection API
Java Reflection API
The Reflection API lets Java code examine and interact with its own structure at runtime — reading class names, listing methods and fields, reading annotations, and invoking methods — all without knowing those details at compile time. Every framework that inspects your classes without you telling it exactly what is in them uses reflection: Spring's component scanner, JUnit's test runner, Hibernate's entity mapper, Jackson's JSON serializer. None of them were compiled knowing the names of your fields or the signatures of your methods, yet they find and use them anyway. Reflection is the mechanism that makes all of that possible.
What Is the Reflection API?
Reflection is provided primarily through four classes in java.lang and java.lang.reflect:
java.lang.Class<T> The entry point for all reflection. Every loaded class, interface, enum, record, and array type has exactly one Class object in the JVM. From it, you can reach everything else. java.lang.reflect.Method Represents one method declared on a class - its name, return type, parameter types, annotations, and modifiers. Can be invoked. java.lang.reflect.Field Represents one field declared on a class - its name, type, annotations, and modifiers. Can be read or written. java.lang.reflect.Constructor Represents one constructor - its parameter types, annotations, and modifiers. Can be used to create new instances.
Basic Overview - What Reflection Can and Cannot Do
GETTING A Class OBJECT (the starting point):
Three ways to get one:
ClassName.class <- compile-time literal, type-safe, preferred
object.getClass() <- from a live object, returns the runtime class
Class.forName("...") <- from a fully-qualified string name at runtime
Class.forName is the one frameworks use: Spring does not import YOUR
classes at compile time - it finds them by scanning for class files
and loading them by string name at startup
WHAT YOU CAN INSPECT (read-only, never needs setAccessible):
class.getName() <- "com.example.PaymentService"
class.getSimpleName() <- "PaymentService"
class.getDeclaredMethods() <- all methods declared in THIS class
class.getMethods() <- all PUBLIC methods incl. inherited
class.getDeclaredFields() <- all fields declared in THIS class
class.getDeclaredConstructors() <- all constructors
class.getAnnotation(X.class) <- class-level annotation X if present
method.getAnnotation(X.class) <- method-level annotation X if present
class.getSuperclass() <- direct superclass
class.getInterfaces() <- directly implemented interfaces
WHAT YOU CAN INVOKE/MODIFY (may need setAccessible(true) for private):
method.invoke(instance, args) <- call a method on an object
field.get(instance) <- read a field's value
field.set(instance, value) <- write a field's value
constructor.newInstance(args) <- create a new object
THE DECLARED vs NON-DECLARED SPLIT:
Fresher view : getDeclaredMethods() gives you what is IN this
class; getMethods() gives you what is CALLABLE
from outside (public, including inherited)
Deeper view : 'Declared' variants return private/protected/
package-private members too, but only from THIS
class - not inherited ones. Non-declared variants
return only public members but across the whole
inheritance hierarchy. For reflection-based
processors that need to reach private fields
(like ORM mappers reading entity fields),
getDeclaredFields() + setAccessible(true) is
the correct combination
setAccessible(true):
Fresher view : tells the JVM to bypass access modifiers for this
specific reflective operation. Required to read or
write private fields and call private methods.
Deeper view : in Java 9+ modules, setAccessible(true) may be
refused if the containing package is not OPEN to
the reflecting module. Frameworks configure module
opens in module-info.java or via command-line
--add-opens to make deep reflection work
Why the Reflection API Matters
The straightforward answer is that frameworks need it. But understanding WHY frameworks need it — rather than just accepting that they do — is what makes the API genuinely useful knowledge rather than trivia.
The fundamental constraint reflection works around is this: code must be compiled before it runs, but frameworks are compiled long before your application code exists. Spring's DI container was compiled years ago. It has no import for PaymentService. It cannot reference PaymentService.class in its own source code because PaymentService did not exist when Spring was written. Yet at startup, Spring finds PaymentService, reads its @Autowired constructors, reads its @Service annotation, and wires it into the application context — because reflection lets it discover and interact with classes using only their names and their annotations, with no compile-time dependency.
The same logic explains every other reflection use case:
Unit test runners. JUnit does not import your test classes. It scans for classes on the test classpath, loads them by name, finds methods annotated @Test via reflection, creates instances via the no-argument constructor, and invokes each test method — all at runtime, with no prior knowledge of what your tests are named.
JSON serialization. Jackson reads the field names and types of your DTO class at runtime to decide what JSON keys to produce. Your DTO class did not exist when Jackson was written. Reflection is how Jackson's ObjectMapper discovers firstName, lastName, and email in UserResponse without anyone telling it explicitly.
Custom validation frameworks. The entire AnnotationValidator from the custom annotations article works through reflection: it inspects field annotations on any class, without knowing the class's fields at compile time, and applies constraints generically.
How the Reflection API Works
Getting Class Objects
1// File: ClassObjectDemo.java
2
3import java.util.List;
4
5public class ClassObjectDemo {
6
7 static class PaymentGateway {
8 private String gatewayName;
9 public void process(String orderId) {}
10 }
11
12 public static void main(String[] args) throws ClassNotFoundException {
13
14 System.out.println("=== Three ways to get a Class object ===");
15
16 // 1. Compile-time class literal - type-safe, preferred when
17 // the class is known at compile time
18 Class<PaymentGateway> byLiteral = PaymentGateway.class;
19 System.out.println("By literal : " + byLiteral.getName());
20
21 // 2. From a live object - returns the RUNTIME class, which
22 // may be a subclass of the declared type
23 PaymentGateway gateway = new PaymentGateway();
24 Class<?> byInstance = gateway.getClass();
25 System.out.println("By instance : " + byInstance.getSimpleName());
26
27 // 3. By fully-qualified string name - how frameworks find
28 // classes they were not compiled against
29 Class<?> byName = Class.forName("java.util.ArrayList");
30 System.out.println("By forName : " + byName.getSimpleName());
31
32 System.out.println();
33
34 System.out.println("=== Class metadata ===");
35 System.out.println("Name : " + byLiteral.getName());
36 System.out.println("Simple name : " + byLiteral.getSimpleName());
37 System.out.println("Package : " + byLiteral.getPackageName());
38 System.out.println("Superclass : " + byLiteral.getSuperclass().getSimpleName());
39 System.out.println("Is interface : " + byLiteral.isInterface());
40 System.out.println("Is enum : " + byLiteral.isEnum());
41
42 System.out.println();
43
44 System.out.println("=== Generic class - inspecting List interface ===");
45 Class<?> listClass = List.class;
46 System.out.println("Is interface : " + listClass.isInterface());
47 System.out.println("Interfaces : " + listClass.getInterfaces().length + " directly implemented");
48 }
49}Output:
=== Three ways to get a Class object ===
By literal : ClassObjectDemo$PaymentGateway
By instance : PaymentGateway
By forName : ArrayList
=== Class metadata ===
Name : ClassObjectDemo$PaymentGateway
Simple name : PaymentGateway
Package :
Superclass : Object
Is interface : false
Is enum : false
=== Generic class - inspecting List interface ===
Is interface : true
Interfaces : 1 directly implemented
Inspecting Methods and Fields
1// File: InspectionDemo.java
2
3import java.lang.reflect.Field;
4import java.lang.reflect.Method;
5import java.lang.reflect.Modifier;
6
7public class InspectionDemo {
8
9 static class OrderService {
10 private final String serviceId = "ORDER-SVC-01";
11 private int processedCount;
12 public String status = "ACTIVE";
13
14 public void processOrder(String orderId, double amount) {}
15 private void validateOrder(String orderId) {}
16 protected boolean checkInventory(String productId) { return true; }
17 public static void resetCounters() {}
18 }
19
20 public static void main(String[] args) {
21 Class<OrderService> clazz = OrderService.class;
22
23 System.out.println("=== getDeclaredMethods() - ALL methods in THIS class ===");
24 for (Method method : clazz.getDeclaredMethods()) {
25 String access = Modifier.toString(method.getModifiers());
26 System.out.printf(" %-12s %-20s params=%d%n",
27 access, method.getName(), method.getParameterCount());
28 }
29
30 System.out.println();
31
32 System.out.println("=== getMethods() - only PUBLIC methods, including inherited ===");
33 int publicCount = 0;
34 for (Method method : clazz.getMethods()) {
35 if (method.getDeclaringClass() == clazz) {
36 System.out.printf(" %-20s (declared in OrderService)%n", method.getName());
37 publicCount++;
38 }
39 }
40 System.out.println(" ... plus " + (clazz.getMethods().length - publicCount)
41 + " inherited public methods from Object");
42
43 System.out.println();
44
45 System.out.println("=== getDeclaredFields() - ALL fields in THIS class ===");
46 for (Field field : clazz.getDeclaredFields()) {
47 String access = Modifier.toString(field.getModifiers());
48 System.out.printf(" %-25s %-15s type=%s%n",
49 access, field.getName(), field.getType().getSimpleName());
50 }
51 }
52}Output:
=== getDeclaredMethods() - ALL methods in THIS class ===
public processOrder params=2
private validateOrder params=1
protected checkInventory params=1
public static resetCounters params=0
=== getMethods() - only PUBLIC methods, including inherited ===
processOrder (declared in OrderService)
resetCounters (declared in OrderService)
status (declared in OrderService)
... plus 9 inherited public methods from Object
=== getDeclaredFields() - ALL fields in THIS class ===
private final serviceId type=String
private processedCount type=int
public status type=String
Invoking Methods and Reading Fields
1// File: InvocationDemo.java
2
3import java.lang.reflect.Field;
4import java.lang.reflect.Method;
5
6public class InvocationDemo {
7
8 static class InventoryService {
9 private int stockCount = 100;
10 private String warehouseId = "WH-NORTH";
11
12 public String checkAvailability(String productId) {
13 return stockCount > 0 ? productId + " in stock (" + stockCount + ")" : productId + " out of stock";
14 }
15
16 private void adjustStock(int delta) {
17 stockCount += delta;
18 System.out.println(" Stock adjusted by " + delta + " -> new count: " + stockCount);
19 }
20 }
21
22 public static void main(String[] args) throws Exception {
23 InventoryService service = new InventoryService();
24
25 System.out.println("=== Invoking a public method via reflection ===");
26 Method checkMethod = InventoryService.class
27 .getDeclaredMethod("checkAvailability", String.class);
28 // invoke(instance, arg1, arg2, ...) - first arg is the object,
29 // rest are the method's parameters
30 Object result = checkMethod.invoke(service, "LAPTOP-PRO-16");
31 System.out.println("Result: " + result);
32
33 System.out.println();
34
35 System.out.println("=== Invoking a PRIVATE method via reflection ===");
36 Method adjustMethod = InventoryService.class
37 .getDeclaredMethod("adjustStock", int.class);
38 // setAccessible(true) overrides access control for this Method object
39 // Required for private/protected methods called from outside the class
40 adjustMethod.setAccessible(true);
41 adjustMethod.invoke(service, -10);
42
43 System.out.println();
44
45 System.out.println("=== Reading a private field via reflection ===");
46 Field stockField = InventoryService.class.getDeclaredField("stockCount");
47 stockField.setAccessible(true);
48 int currentStock = (int) stockField.get(service);
49 System.out.println("stockCount field value: " + currentStock);
50
51 System.out.println();
52
53 System.out.println("=== Writing to a private field via reflection ===");
54 Field warehouseField = InventoryService.class.getDeclaredField("warehouseId");
55 warehouseField.setAccessible(true);
56 warehouseField.set(service, "WH-SOUTH");
57 System.out.println("warehouseId after set: " + warehouseField.get(service));
58
59 System.out.println();
60
61 System.out.println("=== Verify change via the public method ===");
62 System.out.println(checkMethod.invoke(service, "MOUSE-WIRELESS"));
63 }
64}Output:
=== Invoking a public method via reflection ===
Result: LAPTOP-PRO-16 in stock (100)
=== Invoking a PRIVATE method via reflection ===
Stock adjusted by -10 -> new count: 90
=== Reading a private field via reflection ===
stockCount field value: 90
=== Writing to a private field via reflection ===
warehouseId after set: WH-SOUTH
=== Verify change via the public method ===
MOUSE-WIRELESS in stock (90)
Creating Instances via Reflection
1// File: DynamicInstantiationDemo.java
2
3import java.lang.reflect.Constructor;
4
5public class DynamicInstantiationDemo {
6
7 static class NotificationSender {
8 private final String channel;
9 private final String endpoint;
10
11 public NotificationSender(String channel, String endpoint) {
12 this.channel = channel;
13 this.endpoint = endpoint;
14 }
15
16 public void send(String message) {
17 System.out.println(" [" + channel + "] -> " + endpoint + ": " + message);
18 }
19 }
20
21 public static void main(String[] args) throws Exception {
22
23 System.out.println("=== Creating instances dynamically via Constructor reflection ===");
24
25 Constructor<NotificationSender> constructor = NotificationSender.class
26 .getDeclaredConstructor(String.class, String.class);
27
28 // Simulate a factory that creates different channel senders
29 // from configuration data - the class name comes from config,
30 // not hardcoded in this method
31 String[][] channelConfigs = {
32 {"SMS", "sms-gateway.internal"},
33 {"PUSH", "fcm.googleapis.com"},
34 {"EMAIL","smtp.sendgrid.net"}
35 };
36
37 for (String[] config : channelConfigs) {
38 NotificationSender sender = constructor.newInstance(config[0], config[1]);
39 sender.send("Order ORD-8001 confirmed");
40 }
41 }
42}Output:
=== Creating instances dynamically via Constructor reflection ===
[SMS] -> sms-gateway.internal: Order ORD-8001 confirmed
[PUSH] -> fcm.googleapis.com: Order ORD-8001 confirmed
[EMAIL] -> smtp.sendgrid.net: Order ORD-8001 confirmed
Reading Annotations via Reflection
Reading annotations is one of the most common and legitimate uses of reflection in production code. The runtime annotation reading covered in the custom annotations article works entirely through these methods.
1// File: AnnotationReflectionDemo.java
2
3import java.lang.annotation.*;
4import java.lang.reflect.Field;
5import java.lang.reflect.Method;
6
7public class AnnotationReflectionDemo {
8
9 @Retention(RetentionPolicy.RUNTIME)
10 @Target(ElementType.TYPE)
11 @interface ServiceConfig {
12 String name();
13 boolean async() default false;
14 }
15
16 @Retention(RetentionPolicy.RUNTIME)
17 @Target(ElementType.METHOD)
18 @interface RateLimit {
19 int requestsPerMinute() default 60;
20 }
21
22 @Retention(RetentionPolicy.RUNTIME)
23 @Target(ElementType.FIELD)
24 @interface Inject {
25 String qualifier() default "";
26 }
27
28 @ServiceConfig(name = "wallet-service", async = true)
29 static class WalletService {
30
31 @Inject(qualifier = "primary-datasource")
32 private String dataSource;
33
34 @RateLimit(requestsPerMinute = 100)
35 public double getBalance(String userId) { return 0.0; }
36
37 @RateLimit(requestsPerMinute = 10)
38 public boolean transfer(String from, String to, double amount) { return true; }
39
40 public String ping() { return "pong"; }
41 }
42
43 public static void main(String[] args) throws Exception {
44 Class<WalletService> clazz = WalletService.class;
45
46 System.out.println("=== Reading a class-level annotation ===");
47 ServiceConfig config = clazz.getAnnotation(ServiceConfig.class);
48 if (config != null) {
49 System.out.println("Service name : " + config.name());
50 System.out.println("Async : " + config.async());
51 }
52
53 System.out.println();
54
55 System.out.println("=== Reading method-level annotations ===");
56 for (Method method : clazz.getDeclaredMethods()) {
57 RateLimit rateLimit = method.getAnnotation(RateLimit.class);
58 if (rateLimit != null) {
59 System.out.printf(" %-12s rate-limit=%d req/min%n",
60 method.getName(), rateLimit.requestsPerMinute());
61 } else {
62 System.out.printf(" %-12s no rate limit%n", method.getName());
63 }
64 }
65
66 System.out.println();
67
68 System.out.println("=== Reading field-level annotations ===");
69 for (Field field : clazz.getDeclaredFields()) {
70 Inject inject = field.getAnnotation(Inject.class);
71 if (inject != null) {
72 System.out.printf(" @Inject field %-15s qualifier='%s'%n",
73 field.getName(), inject.qualifier());
74 }
75 }
76 }
77}Output:
=== Reading a class-level annotation ===
Service name : wallet-service
Async : true
=== Reading method-level annotations ===
getBalance rate-limit=100 req/min
transfer rate-limit=10 req/min
ping no rate limit
=== Reading field-level annotations ===
@Inject field dataSource qualifier='primary-datasource'
Real-World Example - Meesho Generic Object Mapper
A seller data platform ingests product information from multiple external seller feeds, each with slightly different field names. A @FieldMapping custom annotation marks how each internal model field maps to an external source field, and a generic ObjectMapper uses reflection to apply the mapping at runtime - no if (source == "FLIPKART_FEED") branches, no per-source parsing method, just annotations on the model and one generic mapper that handles them all.
1// File: FieldMapping.java
2
3import java.lang.annotation.ElementType;
4import java.lang.annotation.Retention;
5import java.lang.annotation.RetentionPolicy;
6import java.lang.annotation.Target;
7
8@Retention(RetentionPolicy.RUNTIME)
9@Target(ElementType.FIELD)
10public @interface FieldMapping {
11 String sourceField();
12 boolean required() default false;
13 String defaultValue() default "";
14}1// File: ProductListing.java
2
3public class ProductListing {
4
5 @FieldMapping(sourceField = "product_title", required = true)
6 private String title;
7
8 @FieldMapping(sourceField = "seller_sku", required = true)
9 private String sku;
10
11 @FieldMapping(sourceField = "price_inr", required = true)
12 private String price;
13
14 @FieldMapping(sourceField = "stock_qty", defaultValue = "0")
15 private String stockQuantity;
16
17 @FieldMapping(sourceField = "brand_name", defaultValue = "Unbranded")
18 private String brand;
19
20 // No @FieldMapping - this field is never mapped from external data
21 private String internalNotes;
22
23 @Override
24 public String toString() {
25 return "ProductListing[title=" + title + ", sku=" + sku
26 + ", price=" + price + ", stock=" + stockQuantity
27 + ", brand=" + brand + "]";
28 }
29}1// File: ReflectiveObjectMapper.java
2
3import java.lang.reflect.Field;
4import java.util.ArrayList;
5import java.util.List;
6import java.util.Map;
7
8public class ReflectiveObjectMapper {
9
10 public <T> T map(Map<String, String> sourceData, Class<T> targetClass) throws Exception {
11 T instance = targetClass.getDeclaredConstructor().newInstance();
12 List<String> missingRequired = new ArrayList<>();
13
14 for (Field field : targetClass.getDeclaredFields()) {
15 FieldMapping mapping = field.getAnnotation(FieldMapping.class);
16
17 if (mapping == null) {
18 continue; // skip fields with no @FieldMapping
19 }
20
21 String sourceValue = sourceData.get(mapping.sourceField());
22
23 if (sourceValue == null || sourceValue.isBlank()) {
24 if (mapping.required()) {
25 missingRequired.add(mapping.sourceField());
26 continue;
27 }
28 sourceValue = mapping.defaultValue();
29 }
30
31 field.setAccessible(true);
32 field.set(instance, sourceValue);
33 }
34
35 if (!missingRequired.isEmpty()) {
36 throw new IllegalArgumentException(
37 "Missing required fields: " + missingRequired);
38 }
39
40 return instance;
41 }
42}1// File: SellerFeedMapperDemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class SellerFeedMapperDemo {
7
8 public static void main(String[] args) throws Exception {
9 ReflectiveObjectMapper mapper = new ReflectiveObjectMapper();
10
11 System.out.println("=== Mapping a complete seller feed record ===");
12 Map<String, String> completeFeed = new HashMap<>();
13 completeFeed.put("product_title", "Wireless Noise-Cancelling Headphones");
14 completeFeed.put("seller_sku", "SKU-WNC-2024");
15 completeFeed.put("price_inr", "3499");
16 completeFeed.put("stock_qty", "85");
17 completeFeed.put("brand_name", "SoundWave");
18
19 ProductListing complete = mapper.map(completeFeed, ProductListing.class);
20 System.out.println(complete);
21
22 System.out.println();
23
24 System.out.println("=== Mapping with optional fields absent - defaults applied ===");
25 Map<String, String> partialFeed = new HashMap<>();
26 partialFeed.put("product_title", "USB-C Charging Cable");
27 partialFeed.put("seller_sku", "SKU-USBC-001");
28 partialFeed.put("price_inr", "299");
29 // stock_qty absent -> default "0"
30 // brand_name absent -> default "Unbranded"
31
32 ProductListing partial = mapper.map(partialFeed, ProductListing.class);
33 System.out.println(partial);
34
35 System.out.println();
36
37 System.out.println("=== Mapping with a required field missing - error thrown ===");
38 Map<String, String> invalidFeed = new HashMap<>();
39 invalidFeed.put("product_title", "Bluetooth Speaker");
40 // seller_sku is REQUIRED but absent
41 invalidFeed.put("price_inr", "999");
42
43 try {
44 mapper.map(invalidFeed, ProductListing.class);
45 } catch (IllegalArgumentException e) {
46 System.out.println("Mapping failed: " + e.getMessage());
47 }
48 }
49}Output:
=== Mapping a complete seller feed record ===
ProductListing[title=Wireless Noise-Cancelling Headphones, sku=SKU-WNC-2024, price=3499, stock=85, brand=SoundWave]
=== Mapping with optional fields absent - defaults applied ===
ProductListing[title=USB-C Charging Cable, sku=SKU-USBC-001, price=299, stock=0, brand=Unbranded]
=== Mapping with a required field missing - error thrown ===
Mapping failed: Missing required fields: [seller_sku]
ReflectiveObjectMapper maps any class annotated with @FieldMapping - not just ProductListing. Adding a new seller data model means creating a new class with @FieldMapping annotations, and the same mapper handles it with no changes. This is exactly how Jackson's ObjectMapper, ModelMapper, and MapStruct work - the annotation-plus-reflection pattern at the foundation of every mapping framework.
Reflection API - Key Methods Reference
| Starting Point | Method | Returns | Notes |
|---|---|---|---|
Class | getDeclaredMethods() | Method[] | All methods in this class, all access levels, no inherited |
Class | getMethods() | Method[] | Public methods only, includes inherited |
Class | getDeclaredFields() | Field[] | All fields in this class, all access levels, no inherited |
Class | getDeclaredMethod(name, paramTypes) | Method | Throws NoSuchMethodException if not found |
Class | getDeclaredField(name) | Field | Throws NoSuchFieldException if not found |
Class | getAnnotation(annotationClass) | T extends Annotation | null if absent or retention not RUNTIME |
Class | getDeclaredConstructors() | Constructor[] | All constructors |
Class | getSuperclass() | Class<?> | Direct superclass; null for Object and interfaces |
Method | invoke(instance, args) | Object | null instance for static methods |
Method | getAnnotation(annotationClass) | T extends Annotation | Method-level annotation |
Field | get(instance) | Object | Requires setAccessible(true) for private |
Field | set(instance, value) | void | Requires setAccessible(true) for private |
Best Practices
Prefer compile-time alternatives wherever they exist. Reflection is a runtime mechanism with real costs: it bypasses compiler type-checking, produces Object returns that require casts, and throws checked exceptions that pollute method signatures. When you know the type at compile time, use it directly. Reflection belongs in framework code and tools that genuinely cannot know types at compile time - not in application business logic.
Cache Method, Field, and Constructor objects rather than looking them up repeatedly. getDeclaredMethod() and getDeclaredField() are not free - they scan the class's method/field table and create new wrapper objects. Calling them inside a loop or on every request is a measurable performance tax. Look them up once at startup or initialization, store them in a static final variable or a map, and reuse them.
Call setAccessible(true) once per Method or Field object, not on each call. The access check happens during setAccessible - once it is set, subsequent invoke() or get() calls skip the check. Calling setAccessible(true) every time is redundant and slightly wasteful.
Wrap reflection exceptions with meaningful domain messages before rethrowing. InvocationTargetException, NoSuchMethodException, IllegalAccessException, and IllegalArgumentException are all checked or unchecked exceptions that raw reflection throws. Catching them and rethrowing as IllegalStateException("Failed to read field '" + field.getName() + "' from " + obj.getClass().getSimpleName()) makes the error diagnosable without requiring the caller to understand reflection internals.
In Java 9+ modules, open packages that need deep reflection. setAccessible(true) on a field or method in an unopened module package throws InaccessibleObjectException. If you control the module, add opens com.example.model to your.framework in module-info.java. If you do not, the command-line --add-opens flag is the standard workaround that frameworks document for their users.
Common Mistakes
Mistake 1 - Calling getDeclaredMethod Each Time Instead of Caching
1import java.lang.reflect.Method;
2
3// WRONG - getDeclaredMethod scans the class's method table and
4// constructs a new Method object on every single call. In a hot
5// path (called per request, per item in a batch, inside a loop),
6// this creates measurable garbage and CPU overhead.
7class ReflectionInHotPath {
8 void processEveryOrder(Object service, String orderId) throws Exception {
9 Method method = service.getClass()
10 .getDeclaredMethod("processOrder", String.class); // scans every time
11 method.setAccessible(true);
12 method.invoke(service, orderId);
13 }
14}
15
16// CORRECT - look up the Method object once, at initialization time,
17// and reuse it for every invocation. The setAccessible(true) call
18// also only needs to happen once.
19class ReflectionCached {
20 private final Method processOrderMethod;
21
22 ReflectionCached(Class<?> serviceClass) throws NoSuchMethodException {
23 this.processOrderMethod = serviceClass
24 .getDeclaredMethod("processOrder", String.class);
25 this.processOrderMethod.setAccessible(true);
26 }
27
28 void processEveryOrder(Object service, String orderId) throws Exception {
29 processOrderMethod.invoke(service, orderId); // reuses the cached Method
30 }
31}Mistake 2 - Ignoring InvocationTargetException Inner Cause
1import java.lang.reflect.InvocationTargetException;
2import java.lang.reflect.Method;
3
4// WRONG - catching InvocationTargetException and logging it directly
5// shows the WRAPPER exception, not the REAL exception the method threw.
6// The stack trace points to the reflection call, not the actual error.
7class WrongExceptionHandling {
8 static void invoke(Object target, Method method, Object... args) {
9 try {
10 method.invoke(target, args);
11 } catch (InvocationTargetException e) {
12 System.err.println("Error: " + e.getMessage()); // prints "null" or wrapper info
13 } catch (Exception e) {
14 System.err.println("Error: " + e);
15 }
16 }
17}
18
19// CORRECT - extract getCause() from InvocationTargetException to get
20// the actual exception thrown by the invoked method
21class CorrectExceptionHandling {
22 static void invoke(Object target, Method method, Object... args) throws Exception {
23 try {
24 method.invoke(target, args);
25 } catch (InvocationTargetException ite) {
26 Throwable actualCause = ite.getCause();
27 // rethrow the real cause - this is what callers actually need to see
28 if (actualCause instanceof Exception exception) throw exception;
29 throw new RuntimeException("Unexpected error during reflective invocation", actualCause);
30 }
31 // IllegalAccessException is a programming error - let it propagate
32 }
33}Mistake 3 - Using getMethods() When getDeclaredMethods() Is Needed (or Vice Versa)
1import java.lang.reflect.Method;
2import java.lang.annotation.*;
3
4@Retention(RetentionPolicy.RUNTIME)
5@Target(ElementType.METHOD)
6@interface Internal {}
7
8class BaseService {
9 @Internal
10 public void legacyMethod() {}
11}
12
13class ConcreteService extends BaseService {
14 @Internal
15 public void newMethod() {}
16
17 private void helperMethod() {}
18}
19
20class WrongMethodFetch {
21 static void demo() {
22 // WRONG for finding @Internal on PRIVATE methods -
23 // getMethods() returns only PUBLIC methods, including inherited.
24 // It WILL find legacyMethod() from BaseService (inherited, public)
25 // but WILL NOT find helperMethod() (private, not returned by getMethods())
26 for (Method m : ConcreteService.class.getMethods()) {
27 if (m.isAnnotationPresent(Internal.class)) {
28 System.out.println("Found via getMethods: " + m.getName());
29 // Finds both newMethod and legacyMethod (inherited)
30 // but misses all private methods
31 }
32 }
33
34 // CORRECT for finding annotations on ALL methods in THIS class,
35 // including private - getDeclaredMethods() returns all access levels
36 // but ONLY from ConcreteService itself, not inherited ones
37 for (Method m : ConcreteService.class.getDeclaredMethods()) {
38 if (m.isAnnotationPresent(Internal.class)) {
39 System.out.println("Found via getDeclared: " + m.getName());
40 // Finds newMethod but NOT legacyMethod (that is on BaseService)
41 }
42 }
43 }
44}Mistake 4 - Accessing Fields of a Superclass Via the Subclass
1import java.lang.reflect.Field;
2
3// WRONG - getDeclaredFields() on the SUBCLASS returns fields declared
4// in the subclass ONLY. Fields declared in the SUPERCLASS are not
5// returned even though they are part of the object's state.
6// getDeclaredField("baseField") on the subclass throws NoSuchFieldException.
7class Base {
8 private String baseField = "base-value";
9}
10
11class Child extends Base {
12 private String childField = "child-value";
13}
14
15class WrongFieldAccess {
16 static void demo() throws Exception {
17 Child child = new Child();
18
19 // This throws NoSuchFieldException - "baseField" is on Base, not Child
20 try {
21 Field field = Child.class.getDeclaredField("baseField"); // THROWS
22 } catch (NoSuchFieldException e) {
23 System.out.println("Not found on Child - must look on Base");
24 }
25 }
26}
27
28// CORRECT - walk the class hierarchy to find fields from superclasses
29class CorrectFieldAccess {
30 static Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
31 while (clazz != null) {
32 try {
33 return clazz.getDeclaredField(fieldName);
34 } catch (NoSuchFieldException e) {
35 clazz = clazz.getSuperclass(); // move up to the parent class
36 }
37 }
38 throw new NoSuchFieldException(fieldName);
39 }
40}Interview Questions
Q1. What is the Java Reflection API and what are its primary use cases?
The Reflection API allows Java code to inspect and interact with the structure of classes, methods, fields, and constructors at runtime, without compile-time knowledge of those elements. The primary entry point is java.lang.Class, from which you can obtain Method, Field, and Constructor objects representing program elements. Primary use cases are: framework internals (Spring's dependency injection, JUnit's test runner, Jackson's serialization), annotation-driven processing (reading custom annotations to apply cross-cutting behavior), dynamic proxies and AOP, and tools like IDEs and debuggers that need to inspect arbitrary classes. Application business logic almost never needs reflection directly - it is a framework and tooling mechanism.
Q2. What is the difference between getDeclaredMethods() and getMethods()?
getDeclaredMethods() returns all methods declared directly in the class - public, protected, package-private, and private - but does not include inherited methods from superclasses or interfaces. getMethods() returns only public methods, but includes those inherited from superclasses and interfaces all the way up the hierarchy. For reflection-based processors that need to reach private fields or methods, getDeclaredMethods() combined with setAccessible(true) is the correct pattern. For processors that need to find all callable public APIs including inherited ones, getMethods() is appropriate. Most framework code uses getDeclaredMethods() and walks the class hierarchy manually when needed.
Q3. What does setAccessible(true) do, and when is it required?
setAccessible(true) on a Method, Field, or Constructor object tells the JVM to bypass Java's access control checks for that specific reflective operation. Without it, attempting to call invoke() on a private method or get() on a private field throws IllegalAccessException. It is required whenever reflection is used to access private or protected members from outside their declaring class - which is how ORM frameworks read entity fields, how dependency injection frameworks populate private fields, and how test utilities access internals for assertion. In Java 9+ with the module system, setAccessible may throw InaccessibleObjectException if the package is not opened in module-info.java, requiring explicit module opens.
Q4. Why is reflection performance-sensitive, and how do frameworks mitigate this?
Reflection is slower than direct method calls for several reasons: getDeclaredMethod() and getDeclaredField() scan the class's metadata and construct new objects on every call, invoke() performs access checks and type validation on each invocation, and reflective calls prevent certain JIT compiler optimizations that direct calls benefit from. Frameworks mitigate this by caching Method, Field, and Constructor objects after the first lookup, calling setAccessible(true) once on the cached object, and using MethodHandle (introduced in Java 7) or generated bytecode (as many AOP and ORM frameworks do) for hot paths where the reflective call is made millions of times per second. One-time startup reflection is generally acceptable; per-request or per-item reflection on hot paths requires careful caching.
Q5. What happens when you call method.invoke() and the invoked method throws an exception?
When a method invoked via method.invoke() throws any exception (checked or unchecked), it is wrapped in InvocationTargetException before being thrown from invoke() itself. The actual exception thrown by the method is accessible via invocationTargetException.getCause(). A common mistake is catching InvocationTargetException and logging or rethrowing it directly, which gives the wrong stack trace and obscures the real error. Correct handling always calls getCause() to extract and process the real exception. If the invoked method is declared to throw a specific checked exception and the caller needs to propagate it, getCause() should be cast appropriately before rethrowing.
Q6. How does Class.forName() differ from ClassName.class, and why do frameworks prefer it?
ClassName.class is a compile-time constant - it requires the class to be imported and available at compile time, and the compiler resolves it to the Class object directly. Class.forName("fully.qualified.ClassName") loads the class by string name at runtime - it can load classes that did not exist when the calling code was compiled, and it can be configured externally (from a config file, a database value, a classpath scan). Frameworks prefer Class.forName or classpath scanning because they are compiled independently of application code: Spring's AnnotationConfigApplicationContext finds PaymentService by scanning the classpath for classes annotated @Component, loading each one via its string name, and working with it via the Class object - with no compile-time dependency on PaymentService in Spring's own code.
FAQs
Can reflection access static fields and static methods?
Yes. For static members, pass null as the instance argument to method.invoke(null, args) and field.get(null) or field.set(null, value). The null signals that there is no instance - the operation applies to the class itself. Static private members still require setAccessible(true) to be read or invoked from outside the class.
Can reflection modify a final field?
In Java versions before 17, calling field.setAccessible(true) on a final field and then field.set(instance, value) could change the field's value in the heap object - it bypasses the compiler's final enforcement. However, the JIT compiler may have already inlined the field's original value at any call site that read it as a constant, so the visible effect was unreliable. From Java 17 onward, the JDK strengthened these restrictions as part of the ongoing encapsulation work - the JEP 403 (--illegal-access removed) and related changes make modifying final fields via reflection unreliable and unsupported. Treat final fields as immutable from a reflection standpoint.
What is the difference between getClass() on an object and the .class literal?
object.getClass() returns the runtime class of the object - the actual instantiated type. If PaymentProcessor processor = new UpiProcessor(), then processor.getClass() returns Class<UpiProcessor>, not Class<PaymentProcessor>. PaymentProcessor.class always returns the Class object for PaymentProcessor, regardless of any subclass. For reflection-based processing that should inspect the actual type being used (reading fields of the real object, finding methods the subclass might have overridden), getClass() on the instance is the right choice.
Does reflection work with records?
Yes. A record is a regular class from the JVM's perspective - getDeclaredFields(), getDeclaredMethods(), and getAnnotation() all work on records the same way they work on regular classes. The generated accessor methods (like name(), age()) are visible via getDeclaredMethods(). The canonical constructor is accessible via getDeclaredConstructors(). There are also record-specific methods: clazz.isRecord() returns true for record types, and clazz.getRecordComponents() returns the record's components as RecordComponent[] objects, each carrying the component's name, type, and annotations - useful for annotation-driven mappers that want to work natively with records.
Can reflection be used to call a method on a null object?
Only for static methods - method.invoke(null, args) is valid when the method is static. For instance methods, passing null as the target object throws NullPointerException. This matches what a direct call would do: calling an instance method on null throws NullPointerException regardless of how the call is made.
Is reflection thread-safe?
Reading class structure via getDeclaredMethods(), getDeclaredFields(), getAnnotation(), and similar inspection methods is safe to call concurrently - the Class object's structural metadata does not change after class loading. method.invoke() and field.get/set() are as thread-safe as the underlying method or field they interact with - if the method modifies shared mutable state, concurrent reflective invocations of it carry the same risks as concurrent direct invocations. setAccessible(true) itself is an operation on the Method or Field object - if multiple threads share and concurrently modify the same Method object (calling setAccessible on the same object from different threads), that can be a race. The standard practice is to call setAccessible(true) once during initialization on a dedicated cached object, which eliminates the concern.
Summary
The Reflection API is the runtime mechanism that makes Java frameworks possible - it lets code written without knowledge of your classes inspect those classes, find their annotations, read their fields, and invoke their methods at runtime. The entry point is always a Class object; from it you reach Method, Field, and Constructor objects that represent the class's structure. getDeclaredMethods() and getDeclaredFields() give you everything declared in one class at all access levels; getMethods() and getFields() give you only public members including inherited ones. setAccessible(true) bypasses access control for private members and is required by any code that needs to reach non-public internals.
The practical rules: cache Method and Field objects rather than looking them up per call. Always extract getCause() from InvocationTargetException to see the real error. Walk the class hierarchy explicitly when you need fields from superclasses. And recognize that Class.forName() is the reason frameworks can discover and use your classes without importing them - it is the bridge between a string name in a configuration file or classpath scan and a live, usable type in memory.
Understanding reflection at this level is what separates a developer who uses Spring from one who could build a simple version of it.
What to Read Next
Learn how Java handles errors that happen while a program runs.