Unlike normal Java programming, where the compiler determines the types and members during compilation, reflection enables applications to discover and interact with classes dynamically while the program is executing.
Reflection is the foundation of numerous popular frameworks, including Spring, Hibernate, JUnit, Jackson, Mockito, and many other dependency injection, serialization, ORM, and testing frameworks.
For example, when Spring creates a bean marked with @Service, or when Jackson converts a JSON document into a Java object without explicitly calling constructors or setters in your code, they rely heavily on reflection to inspect classes and invoke methods dynamically.
| Reflection Capability | Example |
|---|---|
| Inspect class metadata | Class name, package, modifiers |
| Inspect constructors | Parameter types, visibility |
| Inspect fields | Public and private fields |
| Inspect methods | Return types, parameters, exceptions |
| Create objects | Instantiate classes dynamically |
| Invoke methods | Call methods without compile-time knowledge |
| Modify fields | Read or update private fields |
| Read annotations | @Entity, @Component, @Test |
For this reason, it should be used only when dynamic behavior is actually required.
In normal Java programming, the compiler already knows the exact type of every object. Method calls, field access, and object creation are verified during compilation, making the code fast and type-safe.Here, the compiler knows everything about the User class. It verifies that the methods exist and generates optimized bytecode for calling them.User user = new User(); user.setName("John"); System.out.println(user.getName());
Reflection is different. Instead of knowing the class beforehand, the application discovers information about the class while it is running.
Suppose the application receives a class name from a configuration file.The compiler has no knowledge of which class will be loaded. The JVM loads the class dynamically at runtime and exposes its metadata through the Reflection API.String className = "com.example.User"; Class<?> clazz = Class.forName(className); System.out.println(clazz.getName());
The Class object
Every loaded class in Java has exactly one corresponding Class object that contains all of its runtime metadata. The Reflection API revolves around this Class object.Using a Class object, developers can inspect constructors, methods, fields, annotations, interfaces, generic information, and much more. There are three common ways to obtain a Class object.
1. The .class syntax is the simplest approach.
Class<String> clazz = String.class;
2. If an object already exists, its Class object can be obtained using getClass().
User user = new User();
Class<?> clazz = user.getClass();
3. When the class name is known only at runtime, use Class.forName().
Class<?> clazz = Class.forName("com.example.User");
All three approaches return the same Class instance for a loaded class.
System.out.println( String.class == "Hello".getClass() );
The output is:
true
Inspecting Class Metadata
Once a Class object is obtained, the Reflection API allows us to inspect nearly every piece of information about the class.This metadata includes the class name, package, modifiers, superclass, implemented interfaces, annotations, generic information, and much more.
The following example retrieves some basic information about a class.
Class<User> clazz = User.class;
System.out.println(clazz.getName());
System.out.println(clazz.getSimpleName());
System.out.println(clazz.getPackageName());
Example output:
com.backendml.User
User
com.backendml
The class modifiers can also be inspected.
int modifiers = clazz.getModifiers();
System.out.println(
Modifier.isPublic(modifiers)
);
System.out.println(
Modifier.isFinal(modifiers)
);
The Modifier utility class converts the modifier bit mask into meaningful information such as public, private, abstract, static, and final.
The superclass can be obtained easily.
Class<?> parent = clazz.getSuperclass();
Similarly, all implemented interfaces can be inspected.
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> i : interfaces) {
System.out.println(i.getName());
}
Reflection can also discover annotations present on the class.
@Entity
public class User {
}
Class<User> clazz = User.class;
System.out.println(
clazz.isAnnotationPresent(Entity.class)
);
Inspecting Constructors
Reflection provides complete access to a class's constructors.Applications can discover constructor visibility, parameter types, annotations, and even create objects dynamically using constructors identified at runtime.
There are two primary methods for retrieving constructors.
1. The getConstructors() method returns only the public constructors.
Constructor<?>[] constructors = User.class.getConstructors();
for (Constructor<?> c : constructors) {
System.out.println(c);
}
2. The getDeclaredConstructors() method returns every constructor, including private, protected, and package-private constructors.
Constructor<?>[] constructors = User.class.getDeclaredConstructors();
for (Constructor<?> c : constructors) {
System.out.println(c);
}
A specific constructor can be retrieved using its parameter types.
Constructor<User> constructor = User.class.getConstructor(
String.class,
int.class
);
Once obtained, the constructor's metadata can be inspected.
System.out.println(
constructor.getParameterCount()
);
for (Parameter parameter : constructor.getParameters()) {
System.out.println(
parameter.getType().getSimpleName()
);
}
Creating Objects Dynamically
One of the most powerful features of reflection is the ability to instantiate classes whose types are not known until runtime.The recommended approach is to obtain a Constructor object and invoke its newInstance() method.
Constructor<User> constructor = User.class.getConstructor(
String.class,
int.class
);
User user = constructor.newInstance(
"John",
30
);
System.out.println(user);
Earlier Java versions also provided Class.newInstance().
User user = User.class.newInstance();
However, this method has been deprecated because it only invokes the no-argument constructor and provides poor exception handling.
Modern applications should always use Constructor.newInstance() instead.
Inspecting Fields
The Reflection API allows applications to inspect every field declared in a class, regardless of whether the field is public, private, protected, or package-private.The getFields() method returns only the public fields, including inherited public fields.
Field[] fields = User.class.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
Most application classes use private fields, so getDeclaredFields() is used much more frequently.
Field[] fields = User.class.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
Once a field is obtained, its metadata can be inspected.
Field field = User.class.getDeclaredField("name");
System.out.println(field.getName());
System.out.println(field.getType());
System.out.println(field.getModifiers());
Reading and Modifying Field Values
Reflection not only discovers fields but also allows their values to be read and modified dynamically. Public fields can be accessed directly using the get() and set() methods.User user = new User();
Field field = User.class.getField("name");
field.set(user, "John");
System.out.println(field.get(user));
Most real-world classes expose private fields, which cannot normally be accessed directly. Reflection provides the setAccessible(true) method to bypass Java access checks.
User user = new User();
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "John");
System.out.println(field.get(user));
Reflection also supports primitive types.
Field ageField = User.class.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(user, 30);
System.out.println(ageField.getInt(user));
The following diagram illustrates the process.
It is primarily intended for infrastructure frameworks rather than normal business logic.
Inspecting Methods
The Reflection API can inspect every method declared in a class, including inherited methods, overloaded methods, private methods, annotations, parameter types, return types, declared exceptions, and modifiers.The getMethods() method returns all public methods, including those inherited from superclasses such as Object.
Method[] methods = User.class.getMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
To retrieve every method declared directly in the class, including private methods, use getDeclaredMethods().
Method[] methods = User.class.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
A specific method can be obtained using its name and parameter types.
Method method = User.class.getMethod(
"setName",
String.class
);
Once retrieved, detailed metadata can be inspected.
System.out.println(method.getName());
System.out.println(method.getReturnType());
for (Parameter parameter : method.getParameters()) {
System.out.println(
parameter.getType().getSimpleName()
);
}
Invoking Methods Dynamically
One of the primary reasons for using reflection is the ability to invoke methods whose names or signatures are not known until runtime.A method is invoked using the Method.invoke() method. The first argument specifies the target object, followed by the method arguments.
User user = new User();
Method method = User.class.getMethod(
"setName",
String.class
);
method.invoke(user, "John");
System.out.println(user.getName());
If the method returns a value, invoke() returns it as an Object.
Method method = User.class.getMethod("getName");
String name = (String) method.invoke(user);
System.out.println(name);
Static methods do not require an object instance. In this case, the first argument passed to invoke() is null.
Method method = Math.class.getMethod(
"max",
int.class,
int.class
);
int result = (Integer) method.invoke(
null,
10,
20
);
System.out.println(result);
Annotations and Reflection
One of the most common uses of reflection is reading annotations at runtime.Reflection can determine whether a class, method, constructor, field, or parameter contains a particular annotation and can also read the annotation's attributes.
Consider a custom annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Service {
String value();
}
The annotation can then be applied to a class.
@Service("userService")
public class UserService {
}
Reflection can inspect the annotation.
Class<UserService> clazz = UserService.class;
Service service = clazz.getAnnotation(Service.class);
System.out.println(service.value());
The output is:
userService
Reflection can also retrieve all annotations present on a class.
Annotation[] annotations = UserService.class.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
Reflection and Generics
Java implements generics using type erasure, meaning that most generic type information is removed after compilation.However, reflection still provides access to certain generic metadata declared in classes, methods, and fields.
For example, consider the following field.
private List<String> names;
Reflection can inspect its generic type.
Field field = User.class.getDeclaredField("names");
Type type = field.getGenericType();
System.out.println(type);
If the field is parameterized, it can be cast to ParameterizedType.
ParameterizedType type = (ParameterizedType) field.getGenericType();
Type[] arguments = type.getActualTypeArguments();
for (Type argument : arguments) {
System.out.println(argument);
}
Output:
class java.lang.String
Arrays and Reflection
The Reflection API also provides support for creating and manipulating arrays dynamically.Instead of using the normal array syntax, reflection uses the Array utility class available in java.lang.reflect. The following example creates an array of strings dynamically.
Object array = Array.newInstance( String.class, 5 );
Since the returned object is of type Object, reflection methods are used to access its elements.
Array.set(array, 0, "John");
Array.set(array, 1, "Alice");
System.out.println(
Array.get(array, 0)
);
System.out.println(
Array.get(array, 1)
);
Reflection can also determine the length of an array.
int length = Array.getLength(array);
System.out.println(length);
Multidimensional arrays can also be created dynamically.
Object matrix = Array.newInstance( int.class, 3, 4 );
System.out.println( Array.getLength(matrix) );
Dynamic Proxies
The Java Reflection API includes support for creating dynamic proxies.A dynamic proxy is an object generated by the JVM at runtime that implements one or more interfaces without requiring a concrete implementation class.
Every method invocation on the proxy is intercepted by an InvocationHandler, allowing applications to execute additional logic before or after the actual method call.
interface GreetingService {
void greet(String name);
}
An invocation handler intercepts all method calls.
InvocationHandler handler = (proxy, method, args) -> {
System.out.println(
"Calling " + method.getName()
);
return null;
};
A proxy object can then be created dynamically.
GreetingService service = (GreetingService) Proxy.newProxyInstance(
GreetingService.class.getClassLoader(),
new Class[] { GreetingService.class },
handler
);
service.greet("John");
Output:
Calling greet
Reflection Performance
Reflection provides tremendous flexibility, but this flexibility comes at a cost.Reflective operations are generally slower than normal Java code because the JVM cannot optimize reflective calls as aggressively as direct method invocations.
A normal method call is resolved during compilation and can be heavily optimized by the JIT (Just-In-Time) compiler.
In contrast, reflective operations require additional runtime work such as locating members, performing security and access checks, boxing and unboxing arguments, and invoking methods indirectly.
Frameworks minimize this cost by performing reflection only during application startup.
After discovering constructors, methods, and fields, they cache the corresponding reflection objects and reuse them throughout the application's lifetime instead of repeatedly performing lookups.
Since Java 7, the MethodHandle API has provided a faster alternative for many reflective operations.
Method handles allow the JVM to optimize dynamic invocations much more effectively, making them preferable for high-performance frameworks that require repeated runtime method calls.
Both Reflection and MethodHandles enable applications to invoke methods dynamically at runtime.
However, they are designed with different goals. Reflection focuses on runtime inspection and dynamic access to class members, whereas MethodHandles provide a lower-level and significantly faster mechanism for dynamic method invocation.
Reflection performs numerous runtime checks whenever a method is invoked.
In contrast, a method handle behaves much more like a direct method reference, allowing the JVM and JIT compiler to optimize repeated invocations much more effectively.
| Reflection | MethodHandles |
|---|---|
| Introduced in Java 1.1 | Introduced in Java 7 |
| Designed for runtime inspection | Designed for high-performance invocation |
| Uses Method.invoke() | Uses MethodHandle.invoke() |
| More runtime overhead | Lower overhead after JVM optimization |
| Rich metadata inspection | Limited metadata capabilities |
| Used by most frameworks | Used by modern JVM libraries and language features |
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(
User.class,
"getName",
MethodType.methodType(String.class)
);
User user = new User("John");
String name = (String) handle.invoke(user);
System.out.println(name);
The following diagram compares the execution path.
Method handles are better suited for scenarios that repeatedly invoke already-discovered methods and require better performance.
Security and Encapsulation
Reflection has the ability to bypass Java's normal access control mechanisms. Using setAccessible(true), applications can access private constructors, methods, and fields that would normally be inaccessible.Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "John");
Although this feature is extremely powerful, it weakens encapsulation by allowing code outside the class to manipulate private implementation details.
Beginning with Java 9, the introduction of the Java Platform Module System (JPMS) significantly strengthened encapsulation.
Even when setAccessible(true) is used, reflective access to classes in another module may be denied unless the module explicitly exports or opens the package.
module my.application {
opens com.example.model;
}
Without the appropriate exports or opens directives, reflective access may result in an InaccessibleObjectException. Because of these restrictions, many modern frameworks request users to configure module access explicitly when running on Java 9 and later.
Reflection in Popular Frameworks
Reflection is one of the core technologies behind nearly every major Java framework.Rather than requiring developers to manually create objects or register every component, frameworks inspect classes at runtime and configure applications automatically.
1. Dependency injection frameworks such as Spring scan packages, locate classes annotated with @Component, @Service, and @Repository, create object instances, and inject dependencies using reflection.
2. ORM frameworks such as Hibernate inspect classes annotated with @Entity, discover fields representing database columns, instantiate entities dynamically, and populate their values while reading data from the database.
3. JSON serialization libraries such as Jackson inspect fields, constructors, getters, setters, and annotations to convert Java objects to JSON and reconstruct Java objects from JSON documents.
4. Testing frameworks such as JUnit discover methods annotated with @Test and execute them automatically without requiring explicit registration.
5. Proxy-based frameworks such as Spring AOP use reflection together with dynamic proxies or bytecode generation libraries to implement transactions, security, logging, caching, and method interception without modifying business code.
Without reflection, most modern Java frameworks would require extensive manual configuration and lose much of the flexibility and automation that developers rely on today.
Final Notes
The Reflection API is one of Java's most powerful runtime capabilities.It enables applications to inspect classes, constructors, methods, fields, annotations, generics, and arrays, as well as create objects and invoke methods dynamically without compile-time knowledge of their types.
This flexibility makes reflection the foundation of many enterprise frameworks, including dependency injection containers, ORM solutions, serialization libraries, testing frameworks, and AOP frameworks.
Although reflective operations are slower than direct Java code and can bypass normal encapsulation, their advantages far outweigh these limitations when building dynamic and extensible systems.