Creating Objects and Calling Methods with Reflection
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
| Method | Description |
|---|---|
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
| Exception | Thrown when |
|---|---|
NoSuchMethodException | No constructor or method matches the name and parameter types |
InstantiationException | The class is abstract or an interface, so no object can be created |
IllegalAccessException | The member isn't accessible (for example, it is private) |
IllegalArgumentException | The arguments have the wrong number or types |
InvocationTargetException | The 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:
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());
}
}
}> 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 mismatchExplanation:
mainnever writesnew Student(...)orb.report(...). The class and methods are found by name, so this code would work for any class with the same shape.invoketakes the target object first, then the arguments. For the staticmaxMarks, the target isnull.- Primitives are boxed:
91is passed as anIntegerand unwrapped; theintresult ofmaxMarkscomes back as anInteger. InvocationTargetException: the constructor threwIllegalArgumentException. Reflection wraps whatever the called code throws, so the real exception ise.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:
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| Point | Class.newInstance() | Constructor.newInstance() |
|---|---|---|
| Which constructors | Only the no-argument one | Any constructor |
| Exceptions from the constructor | Thrown as they are, even checked ones the compiler never saw | Wrapped in InvocationTargetException |
| Private constructors | Not possible | Possible after setAccessible(true) |
| Status | Deprecated since Java 9 | The 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:
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);
}
}PASS addsTwoNumbers
PASS divisionByZeroThrows
FAIL parsesMarks: java.lang.NumberFormatException: For input string: "ninety"
FAIL percentageIsRounded: java.lang.AssertionError: expected 90
2 passed, 2 failedExplanation:
- The annotation:
@Testis kept at run time and allowed only on methods. Itsexpectedelement names an exception the test should throw. Becausenullisn't allowed as a default, the placeholder classNonemeans "no exception expected". - Finding tests:
getDeclaredMethods()returns every method; those without@Test, likehelperIsNotATest, are skipped. The methods are sorted by name because their order isn't guaranteed. - A fresh object per test: each test gets its own
CalculatorTest, so tests can't affect each other. JUnit does the same. - Running:
m.invoke(instance)calls the test. If it returns normally, it passed, unless an exception was expected. - 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
- Easy: Add an
@Ignoreannotation to the runner. Methods with both@Testand@Ignoreshould printSKIP nameand not run. - 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.
Key Takeaways
Was this tutorial helpful?