Java: Reflection API

21 Jul 2026, Updated: 23 Jul 2026 13 min read
1
The Reflection API allows a Java program to inspect and manipulate classes, methods, constructors, fields, annotations, and other metadata at runtime.

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 can inspect almost every aspect of a class, including:
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
Although reflection is extremely flexible, it bypasses many compile-time checks, making the code slower, more complex, and potentially less secure.

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.
User user = new User();
user.setName("John");
System.out.println(user.getName());
Here, the compiler knows everything about the User class. It verifies that the methods exist and generates optimized bytecode for calling them.

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.
String className = "com.example.User";
Class<?> clazz = Class.forName(className);
System.out.println(clazz.getName());
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.

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.

Although this capability is powerful, modifying private fields bypasses encapsulation and should be used carefully.

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.

Suppose an application has the following interface.
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
The following example creates a method handle for a getter method.
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.

Reflection remains the preferred choice when an application needs to discover constructors, fields, methods, annotations, or generic metadata dynamically.

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.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion