Skip to main content
Java · lesson 65 of 66
Open the Java page →
Java intermediate Lesson 65 of 66

Java Reflection: Dynamic Type Inspection and Method Invocation

Learn the Java Reflection API by running it: get Class objects, list members, invoke methods, read private fields, and hit the module-system wall.

By SkillByExample · Updated

Reflection lets a program inspect and use classes it did not know about at compile time: list their members, call methods by name, and read fields — including private ones. Every example is a single file run with the JDK 21 source launcher (java File.java).

Getting a Class object

public class ClassObjectsDemo {
    public static void main(String[] args) throws Exception {
        Class<?> c1 = "hello".getClass();                  // from an instance
        Class<?> c2 = String.class;                        // from a class literal
        Class<?> c3 = Class.forName("java.lang.String");   // from a name at runtime

        System.out.println(c1 == c2);
        System.out.println(c2 == c3);
        System.out.println(c1.getName() + " / " + c1.getSimpleName());
        Class.forName("com.example.Missing");
    }
}
$ java ClassObjectsDemo.java
true
true
java.lang.String / String
Exception in thread "main" java.lang.ClassNotFoundException: com.example.Missing
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
	at jdk.compiler/com.sun.tools.javac.launcher.Main$MemoryClassLoader.loadClass(Main.java:662)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:423)
	at java.base/java.lang.Class.forName(Class.java:414)
	at ClassObjectsDemo.main(ClassObjectsDemo.java:10)

All three routes return the same Class object — a class loader holds one per type, so == works. Only Class.forName can fail at runtime, because the name is just a string: it throws ClassNotFoundException when no such class is on the class path.

Listing fields, methods and constructors

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Comparator;

public class InspectDemo {
    static class Account {
        private final String owner;
        private long balance;
        public Account(String owner) { this.owner = owner; }
        public void deposit(long amount) { balance += amount; }
        public long balance() { return balance; }
        private void audit() {}
    }

    public static void main(String[] args) {
        Class<?> c = Account.class;
        for (Field f : c.getDeclaredFields())
            System.out.println("field  " + Modifier.toString(f.getModifiers()) + " " + f.getType().getSimpleName() + " " + f.getName());
        Arrays.stream(c.getDeclaredMethods())
              .sorted(Comparator.comparing(Method::getName))
              .forEach(m -> System.out.println("method " + Modifier.toString(m.getModifiers()) + " " + m.getName() + Arrays.toString(m.getParameterTypes())));
        for (Constructor<?> k : c.getDeclaredConstructors())
            System.out.println("ctor   " + k);
    }
}
$ java InspectDemo.java
field  private final String owner
field  private long balance
method private audit[]
method public balance[]
method public deposit[long]
ctor   public InspectDemo$Account(java.lang.String)

getDeclared* returns members declared on this class at any access level, which is why the private audit appears; getMethods() would instead return only public members, inherited ones included (toString, hashCode, …). The JVM does not promise any order for these arrays, so the example sorts methods by name before printing.

Invoking a method by name

import java.lang.reflect.Method;

public class InvokeDemo {
    public static class Calculator {
        public int add(int a, int b) { return a + b; }
        public int multiply(int a, int b) { return a * b; }
        public static int negate(int a) { return -a; }
    }

    public static void main(String[] args) throws Exception {
        Calculator calc = new Calculator();
        for (String name : new String[] {"add", "multiply"}) {
            Method m = Calculator.class.getMethod(name, int.class, int.class);
            Object result = m.invoke(calc, 3, 4);
            System.out.println(name + "(3, 4) = " + result + " [" + result.getClass().getSimpleName() + "]");
        }
        Method negate = Calculator.class.getMethod("negate", int.class);
        System.out.println("negate(5) = " + negate.invoke(null, 5));
    }
}
$ java InvokeDemo.java
add(3, 4) = 7 [Integer]
multiply(3, 4) = 12 [Integer]
negate(5) = -5

invoke takes the target object first, then the arguments. Primitive arguments are boxed on the way in and the int result comes back boxed as an Integer. For a static method the target is ignored, so null is passed.

Reading and writing private members

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class PrivateAccessDemo {
    static class Config {
        private String host = "localhost";
        private int port = 8080;
        private String describe() { return host + ":" + port; }
    }

    public static void main(String[] args) throws Exception {
        Config config = new Config();
        Field host = Config.class.getDeclaredField("host");
        host.setAccessible(true);
        System.out.println("host = " + host.get(config));
        host.set(config, "prod.example.com");

        Field port = Config.class.getDeclaredField("port");
        port.setAccessible(true);
        port.setInt(config, 9090);

        Method describe = Config.class.getDeclaredMethod("describe");
        describe.setAccessible(true);
        System.out.println("describe() = " + describe.invoke(config));
    }
}
$ java PrivateAccessDemo.java
host = localhost
describe() = prod.example.com:9090

setAccessible(true) switches off the language’s access check for that one Field or Method object, so private state can be read and changed from outside the class. That is how serializers and test tools work — and why reflection-heavy code is fragile when a private field is renamed.

The module system says no

import java.lang.reflect.Field;

public class ModuleWallDemo {
    public static void main(String[] args) throws Exception {
        Field value = String.class.getDeclaredField("value");
        System.out.println("found: " + value);
        value.setAccessible(true);
        System.out.println("never printed");
    }
}
$ java ModuleWallDemo.java
found: private final byte[] java.lang.String.value
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @7ff2a664
	at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:391)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:367)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:315)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:183)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:177)
	at ModuleWallDemo.main(ModuleWallDemo.java:7)

Finding the private field works, but since Java 16 the JDK’s own packages are strongly encapsulated: setAccessible on java.lang.String internals throws InaccessibleObjectException, because module java.base does not open java.lang. The only way past it is a deliberate opt-in on the command line, --add-opens java.base/java.lang=ALL-UNNAMED — not something library code should need.

Exceptions arrive wrapped

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class WrappedDemo {
    public static class Parser {
        public int parse(String s) { return Integer.parseInt(s); }
    }

    public static void main(String[] args) throws Exception {
        Method parse = Parser.class.getMethod("parse", String.class);
        System.out.println(parse.invoke(new Parser(), "42"));
        try {
            parse.invoke(new Parser(), "forty-two");
        } catch (InvocationTargetException e) {
            System.out.println("wrapper: " + e.getClass().getSimpleName());
            System.out.println("cause:   " + e.getCause());
        }
    }
}
$ java WrappedDemo.java
42
wrapper: InvocationTargetException
cause:   java.lang.NumberFormatException: For input string: "forty-two"

The NumberFormatException thrown inside parse does not escape as itself — invoke wraps it in InvocationTargetException. Code that calls methods reflectively should unwrap getCause(), or error messages end up pointing at the reflection layer instead of the real failure.

Arrays and enums

import java.lang.reflect.Array;

public class ArrayEnumDemo {
    enum Day { MONDAY, TUESDAY, WEDNESDAY }

    public static void main(String[] args) {
        Object array = Array.newInstance(String.class, 3);
        Array.set(array, 0, "first");
        System.out.println(array.getClass().getSimpleName() + " length " + Array.getLength(array));
        System.out.println("component: " + array.getClass().getComponentType().getSimpleName());
        System.out.println("element 0: " + Array.get(array, 0));

        Class<Day> day = Day.class;
        System.out.println("isEnum: " + day.isEnum());
        for (Day d : day.getEnumConstants())
            System.out.println(d + " ordinal " + d.ordinal());
    }
}
$ java ArrayEnumDemo.java
String[] length 3
component: String
element 0: first
isEnum: true
MONDAY ordinal 0
TUESDAY ordinal 1
WEDNESDAY ordinal 2

Array.newInstance builds a real String[] from a Class known only at runtime — what generic code like List.toArray needs, since generics are erased. getEnumConstants returns the constants in declaration order, which is how config and JSON libraries map a string back to an enum.

A note on cost: a reflective call boxes its arguments, checks access and is harder for the JIT to inline. Look Method objects up once, cache them, and keep reflection out of hot loops — or use java.lang.invoke.MethodHandle when you need fast repeated dynamic calls.

Practice

  1. Write dumpFields(Object) that prints every declared field name and value of any object, including private ones.
Solution
import java.lang.reflect.Field;

public class DumpDemo {
    static class Sample {
        private String name = "test";
        private int count = 42;
    }

    static void dumpFields(Object obj) throws IllegalAccessException {
        for (Field f : obj.getClass().getDeclaredFields()) {
            f.setAccessible(true);
            System.out.println(f.getName() + " = " + f.get(obj));
        }
    }

    public static void main(String[] args) throws Exception {
        dumpFields(new Sample());
    }
}
$ java DumpDemo.java
name = test
count = 42
  1. Create an object from a class name held in a String, then call one of its methods by name.
Solution
import java.lang.reflect.Method;

public class CreateByName {
    public static class Greeter {
        public String greet(String name) { return "Hello, " + name; }
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("CreateByName$Greeter");
        Object greeter = clazz.getDeclaredConstructor().newInstance();
        Method greet = clazz.getMethod("greet", String.class);
        System.out.println(greet.invoke(greeter, "Reflection"));
    }
}
$ java CreateByName.java
Hello, Reflection

A nested class’s binary name uses $, so Class.forName needs CreateByName$Greeter, not CreateByName.Greeter.

  1. Write a tiny test runner: run every public no-argument method whose name starts with test on a class, printing PASS or FAIL with the unwrapped cause.
Solution
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

public class MiniRunner {
    public static class MathTests {
        public void testAdd() { if (1 + 1 != 2) throw new AssertionError("add"); }
        public void testDivide() { int zero = 0; System.out.println(1 / zero); }
        public void helper() {}
    }

    public static void main(String[] args) throws Exception {
        Object suite = MathTests.class.getDeclaredConstructor().newInstance();
        Method[] methods = MathTests.class.getMethods();
        Arrays.sort(methods, Comparator.comparing(Method::getName));
        for (Method m : methods) {
            if (!m.getName().startsWith("test") || m.getParameterCount() != 0) continue;
            try {
                m.invoke(suite);
                System.out.println("PASS " + m.getName());
            } catch (InvocationTargetException e) {
                System.out.println("FAIL " + m.getName() + ": " + e.getCause());
            }
        }
    }
}
$ java MiniRunner.java
PASS testAdd
FAIL testDivide: java.lang.ArithmeticException: / by zero

This is, in miniature, what JUnit does — except JUnit finds tests by annotation rather than name.

Frequently Asked Questions

When should I use reflection in Java?
Rarely and deliberately. Reflection suits frameworks and tooling that must work with classes they have never seen, such as dependency injection containers, serializers, test runners and plugin loaders. In ordinary application code it bypasses compile-time checks, breaks encapsulation and is slower than a direct call, so prefer interfaces and lambdas.
Is Java reflection slow?
A reflective call does more work than a direct one: argument boxing into an Object array, access checks, and fewer chances for the JIT to inline. Since JDK 18, Method.invoke is implemented with method handles, which narrowed the gap, but it is still best to look up Method objects once, cache them, and keep reflection out of hot loops.
What does setAccessible(true) do?
It disables Java language access checks for that one reflective object, so you can read or write a private field or call a private method. It works within your own code, but since Java 16 the module system strongly encapsulates JDK internals, so calling it on a private member of java.lang.String throws InaccessibleObjectException unless the package is opened with --add-opens.
What is InvocationTargetException?
When a method called through Method.invoke throws an exception, reflection wraps it in InvocationTargetException. The real exception is available from getCause(). Frameworks usually unwrap it so that stack traces and error messages point at the original failure rather than the reflection layer.