Reflection: Runtime Type Introspection

Reflection is a program's ability to examine and modify its own structure at runtime. Instead of knowing types at compile time, reflective code discovers them dynamically—inspecting classes, methods,...

Key Insights

  • Reflection trades compile-time safety for runtime flexibility, enabling powerful patterns like dependency injection and serialization—but at measurable performance and security costs.
  • Most reflection use cases fall into three categories: framework infrastructure, serialization, and plugin systems. If your use case doesn’t fit these, you probably don’t need reflection.
  • Modern alternatives like source generators and compile-time metaprogramming often provide the same flexibility without runtime overhead—consider them first.

What Is Reflection?

Reflection is a program’s ability to examine and modify its own structure at runtime. Instead of knowing types at compile time, reflective code discovers them dynamically—inspecting classes, methods, fields, and annotations while the program executes.

Consider the difference: statically typed code knows that user.getName() returns a String because the compiler verified it. Reflective code asks “does this object have a method called getName?” and invokes it without compile-time guarantees.

This capability emerged from Lisp’s homoiconicity in the 1960s and became mainstream with Smalltalk-80. Today, most high-level languages support some form of reflection: Java’s java.lang.reflect, Python’s inspect module, C#’s System.Reflection, and even JavaScript’s Reflect API.

The fundamental tradeoff is flexibility versus safety. Reflection lets you write code that adapts to unknown types, but you lose compile-time verification. Errors that would be caught during compilation become runtime exceptions.

Core Reflection Capabilities

Reflection encompasses three distinct capabilities:

Introspection examines types without modifying them. You query an object’s class, enumerate its methods, check for annotations, or inspect parameter types. This is the most common and safest form of reflection.

Intercession modifies behavior at runtime. This includes invoking methods dynamically, setting field values, or creating proxy objects that intercept calls. More powerful, but more dangerous.

Self-modification changes the program’s structure itself—adding methods, modifying classes, or generating new types. Languages vary widely in what they permit here.

The metadata available at runtime depends on the language and compilation settings. Java preserves class structure but erases generic type parameters. Python retains almost everything since it’s interpreted. C++ traditionally offers minimal runtime type information unless you explicitly opt in.

Here’s basic type inspection in action:

// Java: Examining an object's structure
public class TypeInspector {
    public static void inspect(Object obj) {
        Class<?> clazz = obj.getClass();
        
        System.out.println("Class: " + clazz.getName());
        System.out.println("Superclass: " + clazz.getSuperclass().getName());
        
        System.out.println("\nFields:");
        for (Field field : clazz.getDeclaredFields()) {
            System.out.printf("  %s %s%n", 
                field.getType().getSimpleName(), 
                field.getName());
        }
        
        System.out.println("\nMethods:");
        for (Method method : clazz.getDeclaredMethods()) {
            System.out.printf("  %s %s(%d params)%n",
                method.getReturnType().getSimpleName(),
                method.getName(),
                method.getParameterCount());
        }
    }
}
# Python: Same inspection, more concise
import inspect

def inspect_object(obj):
    cls = type(obj)
    print(f"Class: {cls.__name__}")
    print(f"Module: {cls.__module__}")
    
    print("\nAttributes:")
    for name, value in inspect.getmembers(obj):
        if not name.startswith('_'):
            print(f"  {name}: {type(value).__name__}")
    
    print("\nMethods:")
    for name, method in inspect.getmembers(obj, predicate=inspect.ismethod):
        sig = inspect.signature(method)
        print(f"  {name}{sig}")

Common Use Cases

Reflection powers several foundational patterns in modern software:

Dependency Injection Frameworks examine constructor parameters to automatically provide dependencies. Spring, Guice, and .NET’s built-in DI all use reflection to wire objects together without manual instantiation.

Serialization Libraries convert objects to JSON, XML, or binary formats by iterating over fields. Jackson, Gson, and System.Text.Json discover what to serialize through reflection.

ORM Frameworks map database rows to objects by matching column names to field names. Hibernate, Entity Framework, and SQLAlchemy rely heavily on reflective metadata.

Plugin Architectures load and instantiate classes discovered at runtime, enabling extensibility without recompilation.

Testing Frameworks invoke test methods discovered by naming conventions or annotations, and often need to access private state for verification.

Here’s a minimal dependency injection container demonstrating the pattern:

public class SimpleContainer {
    private final Map<Class<?>, Object> instances = new HashMap<>();
    
    public <T> void register(Class<T> type, T instance) {
        instances.put(type, instance);
    }
    
    @SuppressWarnings("unchecked")
    public <T> T resolve(Class<T> type) throws Exception {
        // Return existing instance if registered
        if (instances.containsKey(type)) {
            return (T) instances.get(type);
        }
        
        // Find constructor and resolve its parameters
        Constructor<?> constructor = type.getConstructors()[0];
        Class<?>[] paramTypes = constructor.getParameterTypes();
        Object[] params = new Object[paramTypes.length];
        
        for (int i = 0; i < paramTypes.length; i++) {
            params[i] = resolve(paramTypes[i]); // Recursive resolution
        }
        
        T instance = (T) constructor.newInstance(params);
        instances.put(type, instance);
        return instance;
    }
}

// Usage
class UserRepository { }
class UserService {
    public UserService(UserRepository repo) { }
}

SimpleContainer container = new SimpleContainer();
container.register(UserRepository.class, new UserRepository());
UserService service = container.resolve(UserService.class); // Auto-injects repo

Reflection APIs in Practice

Each language provides distinct APIs for reflection. Understanding the mechanics helps you use them effectively.

Java centers on java.lang.reflect. Every object has a Class obtained via .getClass() or ClassName.class. From there, you access Field, Method, and Constructor objects that represent the type’s structure.

Python uses the inspect module for structured access, but also supports direct attribute manipulation via getattr(), setattr(), and hasattr(). The __dict__ attribute exposes an object’s namespace directly.

C# provides System.Reflection with a similar model to Java, but adds powerful features like expression trees and the dynamic keyword for late binding.

Dynamic method invocation—calling methods by name—is a common reflection task:

// Java: Invoke method by name
public Object invokeMethod(Object target, String methodName, Object... args) 
        throws Exception {
    Class<?>[] paramTypes = Arrays.stream(args)
        .map(Object::getClass)
        .toArray(Class<?>[]::new);
    
    Method method = target.getClass().getMethod(methodName, paramTypes);
    return method.invoke(target, args);
}

// Usage
String result = (String) invokeMethod(someObject, "processData", "input", 42);
# Python: Much simpler dynamic invocation
def invoke_method(target, method_name, *args, **kwargs):
    method = getattr(target, method_name)
    return method(*args, **kwargs)

# Or simply:
result = getattr(some_object, "process_data")("input", 42)
// C#: Using MethodInfo
public object InvokeMethod(object target, string methodName, params object[] args)
{
    var method = target.GetType().GetMethod(methodName);
    return method?.Invoke(target, args);
}

// Or using dynamic (simpler but less control)
dynamic obj = someObject;
var result = obj.ProcessData("input", 42);

Performance Implications

Reflection is slow. Not “slightly slower”—often 10-100x slower than direct calls. Understanding why helps you make informed decisions.

JIT Optimization Barriers: Compilers optimize based on known types. Reflection defeats this by hiding types until runtime. The JIT can’t inline methods, eliminate virtual dispatch, or perform escape analysis.

Security Checks: Each reflective access verifies permissions. Can this caller access this private field? These checks add overhead on every operation.

No Caching by Default: Looking up a Method object repeatedly is expensive. The runtime searches through class metadata each time unless you cache the result.

// Performance comparison
public class ReflectionBenchmark {
    private static final int ITERATIONS = 10_000_000;
    
    public static void main(String[] args) throws Exception {
        Calculator calc = new Calculator();
        Method addMethod = Calculator.class.getMethod("add", int.class, int.class);
        
        // Warm up JIT
        for (int i = 0; i < 10000; i++) {
            calc.add(1, 2);
            addMethod.invoke(calc, 1, 2);
        }
        
        // Direct call
        long start = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            calc.add(1, 2);
        }
        long directTime = System.nanoTime() - start;
        
        // Reflected call (cached Method)
        start = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            addMethod.invoke(calc, 1, 2);
        }
        long reflectedTime = System.nanoTime() - start;
        
        System.out.printf("Direct: %d ms%n", directTime / 1_000_000);
        System.out.printf("Reflected: %d ms%n", reflectedTime / 1_000_000);
        System.out.printf("Ratio: %.1fx slower%n", (double) reflectedTime / directTime);
    }
}

class Calculator {
    public int add(int a, int b) { return a + b; }
}
// Typical output: Reflected calls are 5-20x slower even with cached Method

Mitigation strategies: cache Method and Field objects, use setAccessible(true) to skip access checks (when appropriate), and consider method handles in Java for near-native performance.

Security and Safety Considerations

Reflection can bypass access controls. Calling setAccessible(true) on a private field lets you read and write it directly, breaking encapsulation guarantees the class author intended.

Security Risks:

  • Accessing private fields exposes internal state that might contain sensitive data
  • Dynamic class loading from untrusted sources enables code injection
  • Reflective invocation can call methods the API doesn’t intend to expose

Design Contract Violations:

  • Private fields are private for reasons—invariants, thread safety, future refactoring
  • Reflection-based access couples you to implementation details that may change
  • Testing private methods via reflection often indicates design problems

Modern Java’s module system restricts reflection across module boundaries by default. You must explicitly open packages to reflective access—a deliberate friction against casual reflection use.

Alternatives and When to Avoid Reflection

Before reaching for reflection, consider these alternatives:

Source Generators (C#, Kotlin) generate code at compile time based on annotations or attributes. You get the flexibility of reflection with compile-time safety and zero runtime overhead.

Compile-Time Metaprogramming via macros (Rust, Scala) or annotation processors (Java) moves introspection to build time.

Interfaces Over Introspection: Instead of discovering capabilities via reflection, define interfaces that declare them explicitly.

// Before: Reflection-based plugin loading
public void loadPlugins(List<String> classNames) throws Exception {
    for (String className : classNames) {
        Class<?> clazz = Class.forName(className);
        Method initMethod = clazz.getMethod("initialize");
        Object plugin = clazz.getDeclaredConstructor().newInstance();
        initMethod.invoke(plugin);
    }
}

// After: Interface-based approach
public interface Plugin {
    void initialize();
}

public void loadPlugins(List<Class<? extends Plugin>> pluginClasses) 
        throws Exception {
    for (Class<? extends Plugin> clazz : pluginClasses) {
        Plugin plugin = clazz.getDeclaredConstructor().newInstance();
        plugin.initialize(); // Compile-time verified
    }
}

When reflection is appropriate: framework infrastructure (DI containers, ORMs), serialization libraries, and true plugin systems where types are genuinely unknown at compile time.

When to avoid it: application code that could use interfaces, testing (usually indicates design issues), and anywhere performance matters in hot paths.

The rule of thumb: if you’re writing a framework, reflection might be necessary. If you’re writing an application, it’s probably not. Prefer compile-time solutions that catch errors early and run fast.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.