Java Reflection: Class Introspection, Field and Method Access
Java's reflection API (java.lang.reflect) lets code inspect and interact with classes, fields, methods, and constructors at runtime, even private ones. Reflection powers frameworks like Spring, Hibernate, JUnit, and JSON serializers. It trades compile-time safety for runtime flexibility. Understanding it helps you work with frameworks, write test utilities, and debug library behavior, and helps you recognize when NOT to use it.
Contents
The Class Object
Class<?>Class.classgetClass()Class.forName()Class
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
Inspecting and Accessing Fields
getDeclaredField()setAccessible(true)getField()FieldField.get(instance)Field.set(instance, value)
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)InaccessibleObjectExceptionInvoking Methods Dynamically
getDeclaredMethod()setAccessible(true)Method.invoke(instance, args...)nullInvocationTargetExceptiongetCause()
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");
}
Instantiating via Constructor
getDeclaredConstructor(parameterTypes...)newInstance()ConstructorClass.newInstance()Constructor.newInstance()
// 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());
}
Practical Uses and Limitations
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 MethodHandle
// 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()
MethodHandle