View source on GitHub

Module 21 - Reflection API

Table of contents
  1. Module 21 - Reflection API
    1. Overview
    2. Obtaining a Class Object
    3. Inspecting Classes
      1. getDeclared* vs get*
    4. Reading and Writing Fields
    5. Invoking Methods
    6. Creating Instances
    7. Generic Type Introspection
    8. Dynamic Proxies
      1. Rules
      2. Common patterns
      3. Generic proxy factory
    9. Practical Patterns
      1. Object ↔ Map mapper
      2. Reflective toString / equals / hashCode
      3. Plugin loader
    10. Performance Considerations
    11. When to Use Reflection
    12. Summary

Overview

The Reflection API (java.lang.reflect) lets code inspect and manipulate the structure of other classes at runtime: read/write fields, invoke methods, create instances, and generate proxy objects - all without knowing the types at compile time.

Class Purpose
Class<T> Type metadata - the entry point
Field Instance or static field
Method Instance or static method
Constructor<T> Constructor
Parameter Parameter of a method/constructor
Modifier Int bitmask of access flags
Proxy Runtime-generated proxy that implements interfaces

Obtaining a Class Object

// 1. Compile-time literal
Class<String> c1 = String.class;

// 2. Runtime type of an instance
Class<?> c2 = "hello".getClass();

// 3. Dynamic lookup by fully-qualified name
Class<?> c3 = Class.forName("java.lang.String");

Inspecting Classes

clazz.getSimpleName()          // "ArrayList"
clazz.getCanonicalName()       // "java.util.ArrayList"
clazz.getPackageName()         // "java.util"
clazz.getSuperclass()          // Class of the parent
clazz.getInterfaces()          // directly implemented interfaces
clazz.isInterface()            // true for interfaces
clazz.isEnum()                 // true for enums
clazz.isRecord()               // true for records (Java 16+)
Modifier.isAbstract(clazz.getModifiers())  // true for abstract classes

getDeclared* vs get*

Method Returns
getDeclaredFields() Own fields (any visibility; excludes inherited)
getFields() Public fields of this class and all superclasses
getDeclaredMethods() Own methods (any visibility; excludes inherited)
getMethods() Public methods including inherited ones
getDeclaredConstructors() All constructors of this class

Reading and Writing Fields

Field field = clazz.getDeclaredField("secret");
field.setAccessible(true);     // bypass private access

Object value = field.get(obj);       // read instance field
field.set(obj, newValue);            // write instance field

Object val = field.get(null);        // read static field
field.set(null, newValue);           // write static field

setAccessible(true) bypasses private/protected. In modules it requires an opens declaration or --add-opens JVM flag.


Invoking Methods

Method m = clazz.getDeclaredMethod("greet", String.class);
m.setAccessible(true);
Object result = m.invoke(obj, "Alice");   // instance method

Object result = m.invoke(null, "Alice");  // static method

method.invoke() wraps checked exceptions in InvocationTargetException:

try {
    m.invoke(obj, args);
} catch (InvocationTargetException e) {
    throw e.getCause();   // unwrap the real exception
}

Creating Instances

// Via Constructor
Constructor<Person> ctor = Person.class.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
Person p = ctor.newInstance("Alice", 30);

// No-arg shortcut
Person p2 = Person.class.getDeclaredConstructor().newInstance();

Generic Type Introspection

Type erasure removes generic type parameters at runtime, but they are preserved in class metadata for fields and method signatures:

Field f = MyClass.class.getDeclaredField("items");
ParameterizedType pt = (ParameterizedType) f.getGenericType();
Class<?> typeArg = (Class<?>) pt.getActualTypeArguments()[0];  // e.g. String.class

For a typed subclass:

class StringList extends ArrayList<String> {}

ParameterizedType pt = (ParameterizedType) StringList.class.getGenericSuperclass();
Class<?> typeArg = (Class<?>) pt.getActualTypeArguments()[0];  // String.class

Dynamic Proxies

java.lang.reflect.Proxy generates a proxy class at runtime that implements one or more interfaces. Every method call is dispatched to an InvocationHandler.

Calculator proxy = (Calculator) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (proxyObj, method, args) -> {
        System.out.println("calling " + method.getName());
        return method.invoke(target, args);   // delegate to real object
    }
);

Rules

  • Proxy can only implement interfaces, not extend classes
  • Proxy.isProxyClass(obj.getClass()) - checks if an object is a proxy
  • Proxy.getInvocationHandler(proxy) - retrieves the handler

Common patterns

Pattern How the handler works
Logging Record method name + args, then delegate
Timing Capture System.nanoTime() before/after, then delegate
Caching Return cached result if key (method + args) seen before
Null guard Throw if any arg is null before delegating
Retry Catch exceptions and retry up to N times
Read-only Throw on setter methods; pass getters through

Generic proxy factory

Calculator calc = (Calculator) Proxy.newProxyInstance(
    Calculator.class.getClassLoader(),
    new Class<?>[] { Calculator.class },
    (proxy, method, args) -> switch (method.getName()) {
        case "add"      -> (int) args[0] + (int) args[1];
        case "multiply" -> (int) args[0] * (int) args[1];
        default         -> "mock";
    }
);

Practical Patterns

Object ↔ Map mapper

// Object → Map
Map<String, Object> map = new LinkedHashMap<>();
for (Field f : obj.getClass().getDeclaredFields()) {
    if (Modifier.isStatic(f.getModifiers())) continue;
    f.setAccessible(true);
    map.put(f.getName(), f.get(obj));
}

// Map → Object
for (Field f : obj.getClass().getDeclaredFields()) {
    if (!values.containsKey(f.getName())) continue;
    f.setAccessible(true);
    f.set(obj, values.get(f.getName()));
}

Reflective toString / equals / hashCode

// toString: ClassName{field1=val1, field2=val2}
// equals:   compare each field with Objects.equals
// hashCode: Objects.hash(field1, field2, ...)

Records do this automatically - prefer records over hand-rolled reflection-based equals/hashCode.

Plugin loader

Class<?> pluginClass = Class.forName(className);
if (!expectedType.isAssignableFrom(pluginClass))
    throw new ClassCastException(...);
Constructor<?> ctor = pluginClass.getDeclaredConstructor();
ctor.setAccessible(true);
T plugin = (T) ctor.newInstance();

Performance Considerations

Action Mitigation
Repeated getDeclaredMethod lookups Cache Method / Field objects
setAccessible(true) on each call Call once; the flag persists on the object
Proxy dispatch overhead Profile first; rarely a bottleneck vs business logic
Type erasure for generics Use ParameterizedType via field/method signatures

When to Use Reflection

Good uses:

  • Dependency injection containers (Spring, Guice)
  • Object-relational mappers (Hibernate, JPA)
  • JSON/XML serialisation (Jackson, GSON)
  • Test utilities and mocking (JUnit, Mockito)

Avoid when:

  • You control the types at compile time - use generics or interfaces instead
  • Performance is critical in a hot path
  • You want compile-time safety

Summary

Task API
Get class metadata clazz.getDeclaredFields/Methods/Constructors()
Read/write field field.setAccessible(true); field.get/set(obj)
Invoke method method.setAccessible(true); method.invoke(obj, args)
Create instance ctor.setAccessible(true); ctor.newInstance(args)
Dynamic proxy Proxy.newProxyInstance(loader, interfaces, handler)
Generic type arg (ParameterizedType) field.getGenericType()
Check proxy Proxy.isProxyClass(obj.getClass())