Contents
- The Class Object
- Inspecting and Accessing Fields
- Invoking Methods Dynamically
- Instantiating via Constructor
- Practical Uses and Limitations
Class<?> is the entry point to all reflection. Every loaded class has exactly one Class instance, obtainable via a .class literal on a known type, getClass() on an instance, or Class.forName() with a fully-qualified name (useful when the class name is only known at runtime). From a Class object you can enumerate constructors, methods, fields, annotations, implemented interfaces, and supertype information.
import java.lang.reflect.*;
// Every class has a Class<T> object — the entry point to reflection
// Three ways to get a Class object
Class<String> c1 = String.class; // class literal
Class<?> c2 = "hello".getClass(); // from instance
Class<?> c3 = Class.forName("java.lang.String"); // by fully-qualified name (may throw)
System.out.println(c1 == c2); // true — same Class object
// Class metadata
Class<?> cls = ArrayList.class;
System.out.println(cls.getName()); // java.util.ArrayList
System.out.println(cls.getSimpleName()); // ArrayList
System.out.println(cls.getPackageName()); // java.util
System.out.println(cls.getSuperclass()); // class java.util.AbstractList
System.out.println(cls.isInterface()); // false
System.out.println(cls.isEnum()); // false
System.out.println(cls.isRecord()); // false (Java 16+)
// Implemented interfaces
for (Class<?> iface : cls.getInterfaces()) {
System.out.println(iface.getSimpleName()); // List, RandomAccess, Cloneable, Serializable
}
// Modifiers
int mod = cls.getModifiers();
System.out.println(Modifier.isPublic(mod)); // true
System.out.println(Modifier.isAbstract(mod)); // false
// Checking if one class is assignable from another
System.out.println(List.class.isAssignableFrom(ArrayList.class)); // true
System.out.println(ArrayList.class.isInstance(new ArrayList<>())); // true
getDeclaredField() locates any field declared in the class regardless of access modifier; calling setAccessible(true) on it suppresses the private access check so you can read and write private fields. In contrast, getField() only returns public fields, including those inherited from superclasses. Once you have a Field object, Field.get(instance) reads the value and Field.set(instance, value) writes it.
class Person {
public String name;
private int age;
private static int count = 0;
}
Class<?> cls = Person.class;
// getFields() — public fields (including inherited)
// getDeclaredFields() — all fields (including private), but NOT inherited
Field[] allFields = cls.getDeclaredFields();
for (Field f : allFields) {
System.out.printf("%-10s %-10s %s%n",
Modifier.toString(f.getModifiers()),
f.getType().getSimpleName(),
f.getName());
}
// public String name
// private int age
// private int count (static)
// Access a specific field by name
Field ageField = cls.getDeclaredField("age");
ageField.setAccessible(true); // bypass private access restriction
Person p = new Person();
p.name = "Alice";
// p.age is private — set via reflection:
ageField.set(p, 30);
System.out.println(ageField.get(p)); // 30
// Read a private field
Field nameField = cls.getDeclaredField("name");
nameField.setAccessible(true);
System.out.println(nameField.get(p)); // Alice
// Get field type information
System.out.println(ageField.getType()); // int
System.out.println(ageField.getGenericType()); // int
setAccessible(true) bypasses Java's access controls. In Java 9+, the module system adds additional restrictions — accessing fields in non-exported packages from unnamed modules may throw InaccessibleObjectException. The JVM may also emit warnings about illegal reflective access.
getDeclaredMethod() locates any method by name and parameter types, including private ones; follow it with setAccessible(true) to bypass access control. Method.invoke(instance, args...) calls the method — pass null as the first argument for static methods. Any exception thrown by the invoked method is wrapped in an InvocationTargetException; always unwrap it with getCause() to retrieve the original exception.
class Calculator {
public int add(int a, int b) { return a + b; }
private int multiply(int a, int b) { return a * b; }
public static int square(int n) { return n * n; }
}
Class<?> cls = Calculator.class;
Calculator calc = new Calculator();
// Get and invoke a public method
Method addMethod = cls.getMethod("add", int.class, int.class);
int result = (int) addMethod.invoke(calc, 3, 4);
System.out.println(result); // 7
// Get and invoke a private method
Method mulMethod = cls.getDeclaredMethod("multiply", int.class, int.class);
mulMethod.setAccessible(true);
int product = (int) mulMethod.invoke(calc, 5, 6);
System.out.println(product); // 30
// Invoke a static method — pass null as the instance
Method squareMethod = cls.getMethod("square", int.class);
int sq = (int) squareMethod.invoke(null, 9);
System.out.println(sq); // 81
// getMethods() — all public methods including inherited (from Object etc.)
// getDeclaredMethods() — all methods declared in this class, any access, not inherited
for (Method m : cls.getDeclaredMethods()) {
System.out.println(m.getName() + " ← " +
Arrays.stream(m.getParameterTypes()).map(Class::getSimpleName).toList());
}
// Generic return type
Method m = List.class.getMethod("get", int.class);
System.out.println(m.getReturnType()); // class java.lang.Object
System.out.println(m.getGenericReturnType()); // E (type variable)
// Exception handling in invoke
try {
addMethod.invoke(calc, "wrong", "types"); // passes wrong types
} catch (InvocationTargetException e) {
System.err.println("Wrapped exception: " + e.getCause()); // the actual exception
} catch (IllegalArgumentException e) {
System.err.println("Wrong argument types");
}
getDeclaredConstructor(parameterTypes...) locates a constructor by its exact parameter types; passing no arguments finds the no-arg constructor. Calling newInstance() on the returned Constructor object is preferred over the deprecated Class.newInstance(), which could not handle constructors that declare checked exceptions and silently propagated them. Always use Constructor.newInstance() for reliable error handling.
// newInstance() (deprecated Java 9+) vs getDeclaredConstructor().newInstance()
class Config {
private final String host;
private final int port;
public Config() { this("localhost", 8080); }
public Config(String host, int port) {
this.host = host;
this.port = port;
}
@Override public String toString() { return host + ":" + port; }
}
// Using no-arg constructor
Constructor<Config> noArg = Config.class.getDeclaredConstructor();
Config c1 = noArg.newInstance();
System.out.println(c1); // localhost:8080
// Using parameterized constructor
Constructor<Config> withArgs = Config.class.getDeclaredConstructor(String.class, int.class);
Config c2 = withArgs.newInstance("prod.example.com", 443);
System.out.println(c2); // prod.example.com:443
// Instantiate by class name — factory pattern
String className = "com.example.PluginImpl";
Class<?> pluginClass = Class.forName(className);
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
// Cast to a known interface:
// MyPlugin plugin = (MyPlugin) pluginClass.getDeclaredConstructor().newInstance();
// List all constructors
for (Constructor<?> con : Config.class.getDeclaredConstructors()) {
System.out.println(con.getParameterCount() + " params: " +
Arrays.stream(con.getParameterTypes()).map(Class::getSimpleName).toList());
}
Reflection is the foundation of major Java frameworks: Spring uses it for dependency injection and bean wiring, JUnit for test method discovery, and Jackson for JSON serialization and deserialization. The downsides are significant: reflection is 10–100x slower than direct calls due to argument boxing and access checks, it bypasses compile-time type safety so errors surface only at runtime, and it breaks encapsulation by exposing private internals. Use it for framework and infrastructure code, and prefer interfaces, lambdas, or MethodHandles wherever possible in application logic.
// USE CASE 1: Simple bean-to-map converter (like JSON serializers do)
Map<String, Object> toMap(Object bean) throws Exception {
Map<String, Object> map = new LinkedHashMap<>();
for (Field f : bean.getClass().getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
f.setAccessible(true);
map.put(f.getName(), f.get(bean));
}
return map;
}
// usage: toMap(new Person("Alice", 30)) → {name=Alice, age=30}
// USE CASE 2: JUnit-style test runner — run all methods annotated with @Test
void runTests(Class<?> testClass) throws Exception {
Object instance = testClass.getDeclaredConstructor().newInstance();
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) { // @Test annotation check
System.out.println("Running: " + m.getName());
m.invoke(instance);
}
}
}
// LIMITATIONS and pitfalls:
// 1. Performance — reflection is 10–100x slower than direct calls (invoke overhead)
// Solution: cache Method/Field objects; use MethodHandle for hot paths
// 2. No compile-time safety — typos in field/method names → runtime exceptions
// Solution: use constants or annotation processors
// 3. Module system (Java 9+) — modules can restrict reflective access
// Solution: open packages in module-info.java:
// opens com.example.model to com.framework;
// 4. Breaking encapsulation — avoid in application code; use for frameworks/tools only
// Java 7+ MethodHandle — faster than reflection for repeated calls
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(String.class, "toUpperCase", MethodType.methodType(String.class));
String upper = (String) mh.invoke("hello"); // "HELLO" — faster than Method.invoke()
Reserve reflection for framework and infrastructure code. Application business logic should use direct method calls. For type-safe, high-performance alternatives to reflection, explore MethodHandle (Java 7+) and compile-time code generation with annotation processors.