Java Reflection Examples: Real-World Uses With Code

Java reflection examples from real frameworks, with a test runner, CSV export, DI container, plugin loader, dynamic proxy and the Java 17+ module rules.

The Class object as the entry point to fields, methods, constructors and record components, and what we can do with each

Java reflection is the API in java.lang.Class and java.lang.reflect that lets a running program inspect classes and their fields, methods and constructors, and use them by name, without knowing the types when the code is compiled. The price is lost compile-time checks, slower calls and errors that show up only at runtime.

Application code needs reflection rarely. Frameworks and tools depend on it, because they work with our classes without knowing them in advance. Spring creates our beans, Jackson fills our objects from JSON, Hibernate maps entities to tables and JUnit finds our test methods, all through reflection.

The following example creates an ArrayList from its class name and calls two of its methods by name.

Class<?> type = Class.forName("java.util.ArrayList");
Object list = type.getDeclaredConstructor().newInstance();
Object added = type.getMethod("add", Object.class).invoke(list, "apple");   // true
Object size = type.getMethod("size").invoke(list);                          // 1
String simpleName = type.getSimpleName();                                   // "ArrayList"

Notice that the class name is a plain string, so it could come from a configuration file, and that every result comes back as Object. We look at when reflection is the right tool and build six small versions of what real frameworks do with it, namely a test runner, a CSV exporter, a dependency injection container, a plugin loader, a logging proxy and private field access in tests. After that, we cover the module restrictions, the cost and the alternatives.

1. When Reflection Is the Right Tool

Reflection is the right tool when the code must work with classes that it does not know at compile time. If we know the class, a normal method call is shorter, faster and checked by the compiler. A framework is written years before our classes exist, so it has to discover them at runtime.

The Class object as the entry point to fields, methods, constructors and record components, and what we can do with each
Every reflection task starts from a Class object, and each member type has one action, read or write a field, invoke a method or create an object

The table maps well-known libraries to the reflection calls they rely on. Each row matches one of the examples in this article.

LibraryWhat it does with reflectionExample in this article
JUnit 5, TestNGFinds methods with @Test and invokes themsection 2
Jackson, GsonReads fields, getters or record components to write JSONsection 3
Spring, Jakarta CDICalls constructors and fills their parameters with other beanssection 4
JDBC drivers, logging backendsLoad an implementation class from its namesection 5
Spring AOP, MockitoCreate proxies that intercept method callssection 6
Test helpersRead or set private fieldssection 7

Not every tool that looks at classes uses reflection. IDE code completion and static analysis tools such as SpotBugs, PMD and Checkstyle read source code or class files without loading the classes into the JVM. Reflection works only on classes that are loaded in the running program.

2. Finding and Running Test Methods

A test runner gets a test class it has never seen and must run every method marked as a test. It needs the list of methods, a way to check an annotation on each one and a way to call it. The annotation tutorial explains why the annotation needs RUNTIME retention for this to work.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface TestCase {}
class CartTests {
    @TestCase
    void emptyCartIsFree() {
        int total = 0;
        if (total != 0) throw new AssertionError("expected 0 but was " + total);
    }

    @TestCase
    void oddItemIsNotLost() {
        int total = 7 / 2 * 2;
        if (total != 7) throw new AssertionError("expected 7 but was " + total);
    }

    void helper() {}
}

The runner sorts the methods by name first, because getDeclaredMethods() returns them in no particular order. When a test throws, invoke() wraps the exception in an InvocationTargetException, so the runner reads the real failure from getCause().

static List<String> runTests(Class<?> testClass) throws ReflectiveOperationException {
    Object instance = testClass.getDeclaredConstructor().newInstance();
    Method[] methods = testClass.getDeclaredMethods();
    Arrays.sort(methods, Comparator.comparing(Method::getName));
    List<String> results = new ArrayList<>();
    for (Method method : methods) {
        if (!method.isAnnotationPresent(TestCase.class)) {
            continue;
        }
        try {
            method.invoke(instance);
            results.add("PASS " + method.getName());
        } catch (InvocationTargetException e) {
            results.add("FAIL " + method.getName() + ": " + e.getCause().getMessage());
        }
    }
    return results;
}
List<String> report = runTests(CartTests.class);   // [PASS emptyCartIsFree, FAIL oddItemIsNotLost: expected 7 but was 6]

JUnit 3 did the same with a naming rule, running every public method whose name started with test. JUnit 4 and JUnit 5 switched to annotations, and JUnit 5 also calls setAccessible(true), which is why its test classes and methods can be package-private.

3. Exporting Any Record to CSV

Say an admin page of a bookstore app has an Export button on every table, such as books, members and orders. Writing one CSV method per type repeats the same code. With reflection, one method reads the component names of any record for the header and calls the accessor methods for the rows, which is the same idea that Jackson uses for JSON.

record Book(String title, int pages) {}
record Member(String name, int age) {}
static List<String> toCsv(List<? extends Record> rows) throws ReflectiveOperationException {
    RecordComponent[] columns = rows.getFirst().getClass().getRecordComponents();
    List<String> lines = new ArrayList<>();
    lines.add(Arrays.stream(columns).map(RecordComponent::getName).collect(Collectors.joining(",")));
    for (Record row : rows) {
        List<String> cells = new ArrayList<>();
        for (RecordComponent column : columns) {
            cells.add(String.valueOf(column.getAccessor().invoke(row)));
        }
        lines.add(String.join(",", cells));
    }
    return lines;
}
List<String> books = toCsv(List.of(new Book("Dune", 412), new Book("Emma", 474)));   // [title,pages, Dune,412, Emma,474]
List<String> members = toCsv(List.of(new Member("Lokesh", 37)));                      // [name,age, Lokesh,37]

For records, getRecordComponents() returns the components in declaration order, so the columns are stable. For ordinary classes, there is no such guarantee for fields, and libraries sort them or use annotations to fix the order. A real exporter also escapes commas and quotes in the values.

4. A Tiny Dependency Injection Container

In dependency injection, a class declares what it needs in its constructor, and the container creates those objects first. Reflection gives the container the constructor and its parameter types, so it can build the whole object graph from one class name.

class EmailSender {
    String send(String to) {
        return "welcome mail to " + to;
    }
}

class SignupService {
    private final EmailSender sender;

    SignupService(EmailSender sender) {
        this.sender = sender;
    }

    String register(String name) {
        return sender.send(name);
    }
}
static <T> T create(Class<T> type) throws ReflectiveOperationException {
    Constructor<?> constructor = type.getDeclaredConstructors()[0];
    Class<?>[] parameterTypes = constructor.getParameterTypes();
    Object[] arguments = new Object[parameterTypes.length];
    for (int i = 0; i < parameterTypes.length; i++) {
        arguments[i] = create(parameterTypes[i]);
    }
    return type.cast(constructor.newInstance(arguments));
}
SignupService service = create(SignupService.class);
String mail = service.register("Lokesh");             // "welcome mail to Lokesh"

The container never mentions EmailSender, it finds the type in the constructor signature. Spring adds what this sketch leaves out, such as keeping one instance per bean, choosing between several constructors, resolving interfaces to implementations and detecting circular dependencies.

5. Loading a Plugin by Class Name

Some apps let the operations team choose an implementation in a configuration file, for example exporter=com.example.PdfExporter. The app loads the class by name, checks that it implements the expected interface and creates an instance. Older JDBC code did the same with Class.forName(“org.h2.Driver”).

static <T> T loadPlugin(String className, Class<T> type) throws ReflectiveOperationException {
    Class<? extends T> implementation = Class.forName(className).asSubclass(type);
    return implementation.getDeclaredConstructor().newInstance();
}
CharSequence text = loadPlugin("java.lang.StringBuilder", CharSequence.class);
int length = text.length();                                                   // 0
CharSequence wrongType = loadPlugin("java.util.ArrayList", CharSequence.class); // ClassCastException
CharSequence missing = loadPlugin("com.example.PdfExporter", CharSequence.class); // ClassNotFoundException

The call asSubclass() turns a wrong configuration value into a clear ClassCastException at startup instead of a failure deep inside the app. When the plugin classes ship as JARs, ServiceLoader is the better choice, because each JAR lists its implementations in META-INF/services or in module-info.java, and the app needs no class names at all.

6. Logging Calls With a Dynamic Proxy

A dynamic proxy is an object that the JDK creates at runtime for one or more interfaces. Every call on the proxy goes to an InvocationHandler, which receives the Method object and the arguments and decides what to do. Spring AOP can use JDK proxies for beans that implement interfaces, and the proxy design pattern describes the idea.

interface PaymentGateway {
    String charge(String user, int amount);
}

class FakeGateway implements PaymentGateway {
    public String charge(String user, int amount) {
        return "charged " + user + " " + amount;
    }
}
static <T> T withLogging(T target, Class<T> type, List<String> log) {
    InvocationHandler handler = (proxy, method, args) -> {
        log.add("call " + method.getName() + Arrays.toString(args));
        try {
            return method.invoke(target, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    };
    Object proxy = Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] {type}, handler);
    return type.cast(proxy);
}
List<String> log = new ArrayList<>();
PaymentGateway gateway = withLogging(new FakeGateway(), PaymentGateway.class, log);
String receipt = gateway.charge("Lokesh", 40);        // "charged Lokesh 40"
List<String> calls = log;                             // [call charge[Lokesh, 40]]

The handler unwraps InvocationTargetException, so callers of the proxy see the original exception of the gateway. JDK proxies work only for interfaces. For classes, libraries such as Mockito and Spring generate subclasses with bytecode libraries like Byte Buddy.

7. Reading Private Fields and the Module Rules

Reflection can read and change private fields after a call to setAccessible(true). Tests and serialization libraries use it when a class has no getter for a value they need. In our own code, the call works for any class on the class path.

class Wallet {
    private int balance = 100;
}
Field balanceField = Wallet.class.getDeclaredField("balance");
balanceField.setAccessible(true);
Wallet wallet = new Wallet();
int before = balanceField.getInt(wallet);              // 100
balanceField.setInt(wallet, 250);
int after = balanceField.getInt(wallet);               // 250

Since Java 17, the JDK’s own classes are closed to deep reflection, so setAccessible(true) on a private JDK member throws InaccessibleObjectException. The rule comes from the strong encapsulation of JEP 403 and applies to every package that a module does not open. The method trySetAccessible() returns false instead of throwing, so we can check first.

Field stringBytes = String.class.getDeclaredField("value");
boolean opened = stringBytes.trySetAccessible();       // false
boolean ownField = Wallet.class.getDeclaredField("balance").trySetAccessible(); // true

Two more limits apply to final fields. Record fields and static final fields cannot be changed even after setAccessible(true), and Field.set() throws an IllegalAccessException. For ordinary final instance fields, Java 26 (JEP 500) prints a warning when reflection changes them, as a step toward blocking such changes by default. Libraries that need access to another module’s packages ask for it with –add-opens or an opens clause in module-info.java.

8. Cost of Reflection and the Alternatives

A reflective call does more work than a direct call. It checks access, boxes primitive arguments into an Object[], wraps exceptions and gives the JIT compiler less information. Since Java 18, JEP 416 implements Method, Constructor and Field on top of method handles, which made the cost more even, but the lookup with getMethod() is still slow compared to the call.

Frameworks keep the cost low with a few habits that we can copy.

  • They look up Method, Field and Constructor objects once, often at startup, and cache them in a map per class.
  • They use method handles from java.lang.invoke on hot paths, because a method handle stored in a static final field is optimized like a direct call.
  • They generate code at build time with annotation processors, as Micronaut, Dagger and MapStruct do, so no reflection runs at all.
  • They provide reflection metadata for GraalVM native images, which keep only the classes and members that are listed in that metadata.
class TextLength {
    private static final MethodHandle LENGTH = findLength();

    private static MethodHandle findLength() {
        try {
            return MethodHandles.lookup()
                    .findVirtual(String.class, "length", MethodType.methodType(int.class));
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(e);
        }
    }

    static int of(String text) {
        try {
            return (int) LENGTH.invokeExact(text);
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
}
int appleLength = TextLength.of("apple");      // 5

A method handle is checked when we create it, not on each call, and invokeExact() needs the exact argument and return types, so the JIT compiler can inline the call. For application code that only needs to call one of a few known types, an interface or a switch over a sealed type is still better than either API.

9. Java Reflection Examples FAQs

We mostly meet reflection in a framework error message or in a code review comment, so these answers cover speed, good practice and the access rules since Java 17.

9.1. Is Java reflection slow?

A reflective method call is slower than a direct call, mostly because of argument boxing and access checks, and the member lookup is the slowest part. With cached Method objects, the difference does not matter for code that runs a few thousand times per request. For hot loops, we use method handles or generated code, as shown in section 8.

9.2. Is using reflection a bad practice?

In application code, mostly yes, because it removes compile-time checks and breaks refactoring tools. In frameworks, libraries and test utilities, reflection is the standard tool, because those have to work with classes that do not exist yet when they are written.

9.3. Can reflection access private members in Java 17 and later?

Yes, for our own classes and for any package that a module opens. Private members of JDK classes are no longer accessible, and setAccessible(true) throws InaccessibleObjectException unless the JVM starts with a matching –add-opens option.

9.4. What is the difference between getMethods() and getDeclaredMethods()?

The method getMethods() returns all public methods, including the ones inherited from superclasses and interfaces. The method getDeclaredMethods() returns all methods declared in the class itself, with any access modifier, but no inherited ones. The same pattern applies to fields and constructors.

9.5. Why is Class.newInstance() deprecated?

The method Class.newInstance(), deprecated since Java 9, can throw checked exceptions of the constructor without declaring them. We call getDeclaredConstructor().newInstance() instead, which wraps constructor exceptions in an InvocationTargetException.

10. Conclusion

Reflection lets code inspect classes and call their members by name, which is what test runners, serializers, dependency injection containers, plugin loaders and proxies need. Each of the small examples in this article uses only a handful of calls, namely getDeclaredMethods(), getRecordComponents(), getDeclaredConstructors(), Class.forName(), Proxy.newProxyInstance() and setAccessible().

In application code, we use reflection only when the types are unknown at compile time, cache the lookups, unwrap InvocationTargetException and expect the module system to block private JDK internals. Method handles and build-time code generation cover the cases where speed matters. Related language topics are listed in the Java tutorial.

11. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Java’s considered one of the most pervasive and common coding languages used today. Whether building applications for consumers or servers, the scalable programming language seems to pop up everywhere.

  2. Hi Lokesh,

    This is very nice article. I have one questions. You have mentioned that eclipse uses Reflection for auto -assist option. But reflection work on run time and auto -assist is before run time. Could you please explain how it work ?

  3. @Lokesh.. thanks for providing d importance of reflection and practical usage.
    I want to learn reflection… n I believe that you explain the topics in a simple way…
    so i want to ask.. Is there any tutorial on reflection that u suggest..
    tutorial in ur way wud be better…
    thanks in advance…
    n thanks for sharing ur knowledge..

  4. Hi Lokesh,

    Regarding the Reflection, We used most the project the reflection for the following use cases,

    1. When we wanted to Convert the Data Base Model to DTO object which is used for transfer also used for displaying the front end page – In one word Populating the data.

    2. When we wanted convert the Data back to Model when we wanted to save the data in Data base- In one word Reverse Populator
    import org.apache.commons.beanutils.BeanUtils;

    /**
    * This populator is for populate all simples fields in productData like (String, integer, boolean and so on).
    */
    public class EplusFullProductPopulator extends ProductPopulator
    {
    @Override
    public void populate(final ProductModel source, final ProductData target) throws ConversionException
    {
    for (final Field field : target.getClass().getDeclaredFields())
    {
    try
    {
    BeanUtils.setProperty(target, field.getName(), getProductAttribute(source, field.getName()));
    }
    catch (final IllegalAccessException e)
    {
    //Just ignore the field and keep looking all fields
    }
    catch (final IllegalArgumentException e)
    {
    //Just ignore the field and keep looking all fields
    }
    catch (final InvocationTargetException e)
    {
    //Just ignore the field and keep looking all fields
    }
    }
    }
    }

    Or we can take Advantage of Opebn source Framework which uses same above logic and gives more flexibility when we need data type conversation too i,.e Int to float or String to Integer or Formatted String whatever.
    So to take advantage of such we used a Dozzer Mapper: https://dozer.sourceforge.net/documentation/about.html

    Thanks
    Vinod

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.