Creating Objects and Calling Methods with Reflection

Naveen AnamaneniUpdated · 8 min read

Reflection can do more than look at classes. It can create objects and call methods chosen at run time, from nothing more than a class name and a method name. Constructor.newInstance(...) creates an object, and Method.invoke(...) calls a method. This is exactly what frameworks do: Spring creates your service objects this way, and JUnit calls your test methods this way. In this lesson, you will learn both operations, the exceptions they throw, why the old Class.newInstance() is deprecated, and then you will build a mini test runner that finds and runs methods marked with your own @Test annotation.

Methods

MethodDescription
clazz.getDeclaredConstructor(paramTypes...)It is used to find a constructor by its parameter types (none for the no-argument one).
constructor.newInstance(args...)It creates a new object by calling that constructor.
clazz.getMethod(name, paramTypes...)It finds a public method by name and parameter types.
method.invoke(target, args...)It calls the method on target and returns the result as Object.
method.invoke(null, args...)It calls a static method; no target object is needed.

Exceptions to Expect

ExceptionThrown when
NoSuchMethodExceptionNo constructor or method matches the name and parameter types
InstantiationExceptionThe class is abstract or an interface, so no object can be created
IllegalAccessExceptionThe member isn't accessible (for example, it is private)
IllegalArgumentExceptionThe arguments have the wrong number or types
InvocationTargetExceptionThe constructor or method itself threw an exception; getCause() returns it

All except IllegalArgumentException are checked. The first four extend ReflectiveOperationException, so one catch can handle them together.

Example: Create and Call

Let's see an example that loads a class by name, creates two objects, and calls an instance method and a static method:

Create.java▶ Try it
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
public class Create {
    static class Student {
        String name = "unknown";
        int marks;
 
        public Student() { }
 
        public Student(String name, int marks) {
            if (marks < 0) {
                throw new IllegalArgumentException(
                    "marks can't be negative");
            }
            this.name = name;
            this.marks = marks;
        }
 
        public String report(String prefix) {
            return prefix + name + " scored " + marks;
        }
 
        public static int maxMarks() {
            return 100;
        }
    }
 
    abstract static class Shape {
        public Shape() { }
    }
 
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("Create$Student");
 
        Object a = c.getDeclaredConstructor().newInstance();
        Constructor<?> two =
            c.getDeclaredConstructor(String.class, int.class);
        Object b = two.newInstance("Navika", 91);
 
        Method report = c.getMethod("report", String.class);
        System.out.println(report.invoke(a, "> "));
        System.out.println(report.invoke(b, "> "));
 
        Method max = c.getMethod("maxMarks");
        System.out.println(max.invoke(null));
 
        try {
            two.newInstance("Advitha", -5);
        } catch (InvocationTargetException e) {
            System.out.println("wrapped: " + e.getCause());
        }
        try {
            Shape.class.getDeclaredConstructor().newInstance();
        } catch (InstantiationException e) {
            System.out.println("InstantiationException: abstract class");
        }
        try {
            c.getDeclaredConstructor(int.class);
        } catch (NoSuchMethodException e) {
            System.out.println("NoSuchMethodException: " + e.getMessage());
        }
        try {
            report.invoke(b, 42);
        } catch (IllegalArgumentException e) {
            System.out.println("IllegalArgumentException: "
                + e.getMessage());
        }
    }
}
Output
> unknown scored 0
> Navika scored 91
100
wrapped: java.lang.IllegalArgumentException: marks can't be negative
InstantiationException: abstract class
NoSuchMethodException: Create$Student.<init>(int)
IllegalArgumentException: argument type mismatch

Explanation:

  • main never writes new Student(...) or b.report(...). The class and methods are found by name, so this code would work for any class with the same shape.
  • invoke takes the target object first, then the arguments. For the static maxMarks, the target is null.
  • Primitives are boxed: 91 is passed as an Integer and unwrapped; the int result of maxMarks comes back as an Integer.
  • InvocationTargetException: the constructor threw IllegalArgumentException. Reflection wraps whatever the called code throws, so the real exception is e.getCause().
  • <init> is the JVM's internal name for a constructor.

Why Class.newInstance() Is Deprecated

Older code creates objects with clazz.newInstance(). It has been deprecated since Java 9:

Compiler output (javac -Xlint:deprecation)
Old.java:3: warning: [deprecation] newInstance() in Class has been deprecated
        Object o = String.class.newInstance();
                               ^
  where T is a type-variable:
    T extends Object declared in class Class
1 warning
PointClass.newInstance()Constructor.newInstance()
Which constructorsOnly the no-argument oneAny constructor
Exceptions from the constructorThrown as they are, even checked ones the compiler never sawWrapped in InvocationTargetException
Private constructorsNot possiblePossible after setAccessible(true)
StatusDeprecated since Java 9The recommended way

The second row is the real problem: a constructor's checked exception could escape without any throws clause or catch, bypassing the compiler's checks. Replace clazz.newInstance() with clazz.getDeclaredConstructor().newInstance().

Checkpoint: A Mini Test Runner

Now let's combine custom annotations and reflection into a tiny version of JUnit. The runner finds every method marked @Test, creates a fresh object, calls the method, and reports whether it passed:

MiniTest.java▶ Try it
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
 
public class MiniTest {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface Test {
        Class<? extends Throwable> expected() default None.class;
    }
 
    static class None extends Throwable { }
 
    static void check(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }
 
    static class CalculatorTest {
        @Test
        void addsTwoNumbers() {
            check(2 + 3 == 5, "2 + 3 should be 5");
        }
 
        // Wrong on purpose: 455 out of 500 is 91 percent.
        @Test
        void percentageIsRounded() {
            check(Math.round(455 * 100.0 / 500) == 90, "expected 90");
        }
 
        @Test(expected = ArithmeticException.class)
        void divisionByZeroThrows() {
            int x = 10 / 0;
        }
 
        @Test
        void parsesMarks() {
            Integer.parseInt("ninety");
        }
 
        void helperIsNotATest() {
            throw new IllegalStateException("must never run");
        }
    }
 
    static void run(Class<?> testClass) throws Exception {
        Method[] methods = testClass.getDeclaredMethods();
        Arrays.sort(methods, Comparator.comparing(Method::getName));
        int passed = 0;
        int failed = 0;
        for (Method m : methods) {
            Test test = m.getAnnotation(Test.class);
            if (test == null) {
                continue;
            }
            Object instance =
                testClass.getDeclaredConstructor().newInstance();
            m.setAccessible(true);
            String problem = null;
            try {
                m.invoke(instance);
                if (test.expected() != None.class) {
                    problem = "expected "
                        + test.expected().getSimpleName();
                }
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (!test.expected().isInstance(cause)) {
                    problem = cause.toString();
                }
            }
            if (problem == null) {
                passed++;
                System.out.println("PASS " + m.getName());
            } else {
                failed++;
                System.out.println("FAIL " + m.getName() + ": " + problem);
            }
        }
        System.out.println(passed + " passed, " + failed + " failed");
    }
 
    public static void main(String[] args) throws Exception {
        run(CalculatorTest.class);
    }
}
Output
PASS addsTwoNumbers
PASS divisionByZeroThrows
FAIL parsesMarks: java.lang.NumberFormatException: For input string: "ninety"
FAIL percentageIsRounded: java.lang.AssertionError: expected 90
2 passed, 2 failed

Explanation:

  1. The annotation: @Test is kept at run time and allowed only on methods. Its expected element names an exception the test should throw. Because null isn't allowed as a default, the placeholder class None means "no exception expected".
  2. Finding tests: getDeclaredMethods() returns every method; those without @Test, like helperIsNotATest, are skipped. The methods are sorted by name because their order isn't guaranteed.
  3. A fresh object per test: each test gets its own CalculatorTest, so tests can't affect each other. JUnit does the same.
  4. Running: m.invoke(instance) calls the test. If it returns normally, it passed, unless an exception was expected.
  5. Failures: anything the test throws arrives as InvocationTargetException. If the cause is the expected type, the test passed; otherwise the cause becomes the failure message.

This is the heart of every test framework. Real ones add setup and cleanup methods, better reports and parallel runs, but the idea is the same.

Common Mistakes

Catching InvocationTargetException and printing it instead of its cause. The useful information is in getCause().

Using Class.newInstance(). It's deprecated and lets checked exceptions escape unchecked.

Passing the wrong argument types to invoke. You get IllegalArgumentException: argument type mismatch at run time, not a compile error.

Forgetting that invoke returns Object. Cast the result, and expect primitives to come back boxed.

Interview Corner

How do you create an object using reflection?

Get the Class (for example with Class.forName), find a constructor with getDeclaredConstructor(parameterTypes...), and call newInstance(args...) on it. Exceptions thrown by the constructor are wrapped in InvocationTargetException. Class.newInstance() is deprecated since Java 9 because it only supports the no-argument constructor and rethrows the constructor's exceptions, including checked ones, without wrapping them.

How does a framework like JUnit find and run tests?

It scans test classes with reflection, collecting methods that carry the @Test annotation (which has runtime retention). For each, it creates an instance of the test class, calls the method with Method.invoke, and treats a normal return as a pass and an InvocationTargetException as a failure whose cause is the test's exception.

Practice

  1. Easy: Add an @Ignore annotation to the runner. Methods with both @Test and @Ignore should print SKIP name and not run.
  2. Harder: Add @BeforeEach: a method with this annotation runs on the fresh instance before every test. Use it to set up a field that the tests read.

Quick check: Creating objects with reflection

4 questions
1Which is the recommended way to create an object reflectively?
2A method called with invoke throws IllegalStateException. What does the caller catch?
3How do you invoke a static method?
4Why does the test runner create a new object for each test?

Key Takeaways

Was this tutorial helpful?

Lesson 468 of 482