Skip to content

Java Reflection: Runtime Introspection, Serialization & Dependency Injection


Goal of this Chapter This is not "here's the java.lang.reflect API." Reflection is the mechanism that lets frameworks do things your own code structurally can't โ€” inspect a class it has never seen before, at runtime, and act on it. The goal is to understand why frameworks need this escape hatch from the type system, and why it stays an escape hatch rather than something you reach for in business logic.

Crisp Definition

Reflection lets Java inspect and manipulate classes, fields, methods, and constructors at runtime, even when none of that structure was known at compile time.


What Problem Does Reflection Solve?

Engineering Problem

Normally in Java you know class names, methods, and fields, and everything is checked at compile time. But sometimes you genuinely don't know which class will be loaded, which methods exist, or which fields exist until runtime โ€” a plugin system loading a class by name from config, a serializer handed an arbitrary object, a framework instantiating whatever class you annotated. Compile-time-only tooling has no way to act on types it has never seen.

Definition

Reflection lets Java inspect itself at runtime โ€” treat a class, its fields, and its methods as data you can query and invoke programmatically, instead of only writing code against them directly.

Engineering Insight

Reflection trades the compiler's static guarantees for runtime flexibility. That trade is exactly why it belongs at framework boundaries (where the framework genuinely can't know your types in advance) and not inside business logic (where the types are always known, and the compiler's checks are a feature, not an obstacle).


The Meta-Class: Class<T>

Core Idea

Objects have data. Classes also have data โ€” their own structure โ€” and that data is represented by an object of type Class.

public static void main(String[] args) {
    String s = new String();
    Class<?> cls = s.getClass();
    System.out.println(cls.getName());
}
// Output: java.lang.String

Definition

Class<?> cls = obj.getClass(); returns an object representing the structure of the class โ€” it knows the class name, fields, methods, constructors, and modifiers. Class is just a normal Java class, not magic:

public class Object {
    public final Class<?> getClass() { ... }
}

Every class extends Object โ€” therefore every object has getClass().

  • .getName() โ†’ returns the fully qualified class name.

Engineering Insight

Every object carries a reference to a description of its own type, reachable through one universal method. That's what makes reflection possible in the first place: the metadata isn't bolted on externally, it's part of what Object guarantees to every class in the language.


Inspecting Structure โ€” Fields, Methods, Constructors

Call Returns
cls.getDeclaredFields() Only fields declared in this class โ€” includes private/protected/public, excludes inherited fields
cls.getDeclaredMethods() Same as fields โ€” declared in this class only, all access levels, no inherited methods
cls.getMethods() All public methods of the current class, all superclasses, and all implemented interfaces โ€” including inherited ones like toString(), equals(), hashCode() from Object
cls.getDeclaredConstructors() Constructors declared in this class

Walking the Class Hierarchy โ€” getSuperclass()

Class<?> parent = cls.getSuperclass();

Returns the direct (immediate) parent class of the given class.

public class ReflectionDemo {
    public static void main(String[] args) {
        ReflectionDemo s = new ReflectionDemo();
        Class<?> cls = s.getClass();
        while (cls != null) {   // after reaching Object, the next getSuperclass() is null
            Method[] m = cls.getDeclaredMethods();
            System.out.println("\n" + cls.getName());
            for (Method method : m)
                System.out.println(method);
            cls = cls.getSuperclass();
        }
    }
}

Why Interfaces Aren't Returned by getSuperclass()

Java allows only one superclass โ€” getSuperclass() walks a single chain. Interfaces are a separate relationship entirely, retrieved with cls.getInterfaces().


Why Reflection Is Powerful (and Dangerous)

Power Danger
Frameworks (Spring, Hibernate) Breaks encapsulation
Dependency Injection Slower than normal calls
Serialization Compile-time safety lost
ORM Harder to debug
Test frameworks (JUnit) โ€”

Engineering Insight

This is exactly why reflection is used by frameworks, not daily business code โ€” frameworks accept the cost because they're solving a genuinely generic problem (act on types you don't know yet); business code almost always knows its types at compile time, so paying reflection's cost there buys nothing.


Invoking Methods via Reflection โ€” Access Control

Engineering Problem

getDeclaredMethod() can locate a private method โ€” but locating isn't the same as being allowed to call it. If reflection could bypass private for free, access modifiers would mean nothing at runtime.

Definition

Method m = cls.getDeclaredMethod("methodName", paramTypes...);
  • Returns a method declared in the class itself, including private, protected, default, and public.
  • Does not include inherited methods.
  • Access checks are still enforced by default.

The Failure โ€” Access Checks Still Apply

class Secret {
    private void hiddenMethod() {
        System.out.println("Hidden method executed");
    }
}

public class TestReflection {
    public static void main(String[] args) throws Exception {
        Secret obj = new Secret();
        Class<?> cls = obj.getClass();
        Method m = cls.getDeclaredMethod("hiddenMethod");
        m.invoke(obj);   // โœ˜ fails
    }
}
// Runtime Error: java.lang.IllegalAccessException

Reason: Java does not allow invoking private members reflectively by default โ€” you can find a private method, but you cannot invoke it without one more step.

The Fix โ€” setAccessible(true)

Method m = cls.getDeclaredMethod("hiddenMethod");
m.setAccessible(true);   // override access checks
m.invoke(obj);           // now it works
// Output: Hidden method executed

What setAccessible(true) really does:

  • Disables Java language access checks.
  • Allows access to private methods, private fields, private constructors.
  • Works at runtime only โ€” the compiler's own checks are untouched.
  • Bypasses encapsulation.

Why This Is Dangerous

Reflection with setAccessible(true):

  • breaks encapsulation,
  • can access sensitive data,
  • can violate class invariants,
  • makes code fragile and unsafe,
  • is harder to debug, and
  • is slower than normal calls.

That's why reflection โ€” especially with setAccessible(true) โ€” is used by frameworks, not business logic.

Security Aspect

setAccessible(true) can throw SecurityException when a SecurityManager is installed or the environment forbids reflective access โ€” common in secure JVMs, application servers, and sandboxed environments.

Interview one-liner: getDeclaredMethod() retrieves methods declared in a class, but private methods require setAccessible(true) to bypass access checks, which can throw SecurityException and is potentially unsafe.


Specifying Parameter Types โ€” Primitive Class Literals

Method Signature

Method m = cls.getDeclaredMethod(String name, Class<?>... parameterTypes);
  • First argument โ†’ the method name.
  • Second argument โ†’ one Class object per parameter, in order โ€” order must match exactly.

int Is Not a Class โ€” But Java Provides Primitive Class Literals

int.class
double.class
boolean.class

These are special JVM-backed Class objects, not wrapper classes โ€” they let you describe a primitive parameter type to a reflective API that otherwise only deals in Class<?> objects.

class Calculator {
    private int add(int a, int b) {
        return a + b;
    }
    private String concat(String a, int b) {
        return a + b;
    }
}

Class<?> cls = Calculator.class;

Method m1 = cls.getDeclaredMethod("add", int.class, int.class);        // matches add(int, int)
Method m2 = cls.getDeclaredMethod("concat", String.class, int.class);  // order and types must match exactly

Invoking the Method

Object result = m.invoke(Object target, Object... args);
Calculator obj = new Calculator();
Object res = m1.invoke(obj, 3, 4);
System.out.println(res);   // 7

Important details:

  • Arguments are passed as Objects โ€” Java automatically boxes primitives (3 โ†’ Integer.valueOf(3)) at compile time, before your program ever runs.
  • The return value is always Object โ€” cast it if you need the concrete type.

Working example, end to end:

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        Calculator obj = new Calculator();
        Class<?> cls = obj.getClass();
        Method m = cls.getDeclaredMethod("add", int.class, int.class);
        m.setAccessible(true);
        Object result = m.invoke(obj, 5, 7);
        System.out.println(result);   // 12
    }
}

Side Note โ€” Why a Primitive Array Doesn't Satisfy a Varargs Generic

The same autoboxing/erasure mechanics that make invoke(Object target, Object... args) work also explain a common surprise elsewhere:

@SafeVarargs
static <T> void printArray(T... a) {
    for (T t : a) System.out.print(t + " ");
}

int[] arr = {1, 2, 3, 4, 5};
printArray(arr);   // does NOT print 1 2 3 4 5

Rule: generics work when the elements are reference types, not when the container is a reference type. int[] is itself a reference type โ€” so Java infers T = int[], and the whole array becomes a single element, not five. A String[] doesn't have this problem, because its elements (String) are already reference types.

Final mental model:

Generics force reference types.
Varargs only bundle same-type values into an array.
Autoboxing happens at compile time, not runtime.
Runtime only sees objects, due to type erasure.

Varargs decide how many, generics decide what kind,
and autoboxing happens before runtime.

Reflection Use Case 1 โ€” Serialization

Engineering Problem

An object living in JVM heap memory is useless to a REST API, a cache, or another microservice โ€” none of them can read Java heap objects directly. Something has to convert an object into a transferable format, and convert it back.

Definition

Serialization converts an object into a transferable format:

Java Object โ†’ JSON โ†’ bytes (over network / file)

Deserialization is the reverse: bytes / JSON โ†’ Java Object.

Why We Need It

  • APIs (REST)
  • Network communication
  • Caching (Redis)
  • File storage
  • Microservices talking to each other

Where Reflection Fits In

At runtime, generic serialization code doesn't know a given object's fields, getters/setters, or annotations in advance. Serialization uses reflection to inspect the class structure, read private fields, and call getters/setters dynamically โ€” exactly the "types unknown until runtime" problem reflection exists for.

Why Serialization Is Complicated

  • Nested objects
  • Lists / Maps
  • Null handling
  • Custom field names
  • Date formats
  • Ignoring fields
  • Circular references

That's why we don't hand-write it in real projects โ€” Jackson (and libraries like it) does this internally: heavy use of reflection, converting Object โ†” JSON โ†” bytes while honoring annotations.

Interview gold lines:

  • "Serialization converts objects into a transferable format like JSON or bytes."
  • "Reflection enables runtime inspection, which serializers use to access fields dynamically."
  • "Spring uses Jackson internally for request/response body serialization."
  • "We avoid manual serialization due to complexity and performance concerns."

A Minimal Hand-Rolled Serializer

package serialization;

import java.lang.reflect.*;

public class Serializer {
    public static String serialise(Object obj) throws Exception {
        String output;
        Class<?> cls = obj.getClass();
        output = cls.getName() + ":{";
        Field[] fields = cls.getDeclaredFields();
        for (Field f : fields) {
            Class<?> fieldType = f.getType();
            if (Modifier.isTransient(f.getModifiers()))   // skip transient fields
                continue;
            f.setAccessible(true);
            if (fieldType.isPrimitive()) {
                output += f.getName() + ":" + f.get(obj) + ",";
            } else if (fieldType.isArray()) {
                // left as an exercise โ€” arrays need their own recursive handling
            } else {
                output += serialise(f.get(obj));
            }
        }
        output = output.substring(0, output.length() - 1) + "}";
        return output;
    }
}
serialization.ReportCard:{id:1,serialization.ScienceMarks:
{phyMarks:0,chemMarks:0,mathMarks:0,sciPercent:0.0}serialization.ArtsMarks:
{socialSciMarks:0,literatureMarks:0,artsPercent:0.0}totalPercent:0.0}

What a Senior Dev Immediately Guards Against

  • Cyclic references (this recursive version would stack-overflow on one)
  • null handling
  • Static fields (should almost always be excluded)
  • Transient fields (already handled above)
  • Inheritance (getSuperclass() โ€” declared fields alone miss inherited ones)
  • Collections / arrays
  • Performance (string concatenation in a loop โ†’ StringBuilder)
  • Stack overflow risk on deep/cyclic structures
  • Security concerns of setAccessible(true)
  • API contract โ€” what format, deterministic field order?

Engineering Insight

This toy serializer demonstrates why Jackson exists rather than replacing it โ€” every item in the guard-against list above is a real production concern that a hand-rolled reflective serializer has to solve from scratch, and a mature library already has.


Reflection Use Case 2 โ€” Deserialization & Runtime Class Loading

Engineering Problem

Serialized data is not executable code โ€” it's a byte stream that references a class by name. Deserialization needs the actual class definition available at runtime to reconstruct an object; it can't reconstruct behavior it has no class metadata for.

Compile-Time Classpath vs. Runtime Classpath

Compile-Time Classpath Runtime Classpath
Used by The compiler (javac) The JVM
Needed to Compile source code, resolve symbols Load classes, deserialize objects, execute bytecode
Sufficient for deserialization? No Yes โ€” required

If the class is missing from the runtime classpath โ†’ ClassNotFoundException.

Deserialization โ€” Key Flow

  1. The serialized stream contains the Fully Qualified Class Name (FQCN).
  2. The JVM tries to load that class via the runtime classpath.
  3. If found โ†’ class loaded, object reconstructed.
  4. If not found โ†’ deserialization fails.

Class.forName() โ€” Dynamic Class Loading

Class<?> cls = Class.forName("com.example.User");
  • Loads the class at runtime.
  • Requires the FQCN, and the class must be present on the runtime classpath.
  • Triggers class loading and static initialization.

Creating an Object at Runtime (Reflection)

Class<?> cls = Class.forName("com.example.User");
Object obj = cls.getDeclaredConstructor().newInstance();

Used when the class name is only known at runtime โ€” frameworks, serializers, plugins.

Deprecated way:

Class<?> cls = Class.forName("serialization.ReportCard");
Object o = cls.newInstance();   // deprecated

Correct way:

Class<?> cls = Class.forName("serialization.ReportCard");
Constructor<?> constructor = cls.getConstructor();
Object o = constructor.newInstance();

Relation to Deserialization

Deserialization internally uses the class name found in the stream plus the runtime class loader โ€” conceptually the same mechanism as Class.forName(fqcn). If the class isn't on the runtime classpath, it fails the same way.

Common Interview Errors to Avoid

  • Compile-time classpath โ‰  runtime classpath.
  • "Class compiled" โ‰  "class available at runtime" (the class could be missing from the deployed artifact).
  • A serialized object โ‰  executable code โ€” it's data describing an object, requiring the class definition separately.

One-line interview answer: "Deserialization requires the class to be present on the runtime classpath; the JVM loads it using the fully qualified class name, similar to Class.forName()."


Reflection Use Case 3 โ€” Dependency Injection (Spring)

Engineering Problem

Sometimes you want exactly one instance of a class to exist for the whole application, with every thread sharing it โ€” and you want the objects that depend on each other wired together without every class hand-writing new SomeDependency() everywhere it's needed, which would hardcode the dependency graph into every constructor.

Definition

A Spring Bean is an object whose entire lifecycle โ€” creation, dependency injection, scope, destruction โ€” is managed by the Spring container, not by your own code calling new.

Spring Singleton โ‰  JVM Singleton

  • Spring singleton โ†’ one instance per Spring container.
  • Multiple containers โ†’ multiple instances are possible; "singleton" here is scoped to the container, not to the JVM process.

Interview one-liner: "In Spring, a singleton bean is a container-managed object where only one instance exists per application context and is shared across all threads."

@Component
class UserService { }

@Component marks a class as a Spring bean candidate, so Spring can instantiate and manage its object lifecycle.

Autowiring

Autowiring is Spring's mechanism for automatically resolving and injecting bean dependencies at runtime. If a bean DBAccessor depends on another bean Logger, Spring โ€” using the application context โ€” locates a suitable Logger bean and injects it into DBAccessor (typically via constructor injection, or alternatively field/setter injection).

The wiring decision is made based on type first; if multiple candidates exist, qualifiers or bean names disambiguate. This removes manual object creation (new), keeps classes loosely coupled, and lets Spring control object relationships and lifecycle consistently across the application.

Behind the Scenes โ€” Reflection + Graph Resolution

Spring enables autowiring by building a dependency graph of bean definitions and resolving it at runtime:

  • It uses reflection to inspect constructors, fields, and methods (e.g. annotated with @Autowired).
  • It builds a directed graph where nodes are beans and edges represent dependencies.
  • During context initialization, Spring traverses this graph, determines a valid instantiation order via topological sort, creates beans accordingly, and injects dependencies via reflection.

Reflection is used for inspection and injection; the graph-based resolution is what ensures correct ordering, detects missing dependencies, and identifies circular dependencies (with specific handling rules).

Engineering Insight

This splits cleanly into two separate responsibilities: reflection answers "what does this bean need, and how do I set it?", while the dependency graph and topological sort answer "in what order can these beans even be constructed?" Conflating the two would make Spring's core both harder to reason about and harder to test โ€” a clean instance of the same Single-Responsibility reasoning that shows up everywhere else in this course.


Interview Q&A

What problem does reflection actually solve?

It lets code inspect and act on classes, methods, and fields it didn't know about at compile time โ€” needed whenever a class name, method, or field is only knowable at runtime (plugins, serializers, DI containers).

What's the difference between getDeclaredFields()/getDeclaredMethods() and getFields()/getMethods()?

The getDeclared* variants return only members declared directly in that class, at any access level, excluding inherited ones. The non-declared variants (getMethods(), getFields()) return all public members, including ones inherited from superclasses and interfaces.

Why doesn't getSuperclass() return interfaces?

Because Java only allows a single superclass โ€” getSuperclass() walks that one chain. Interfaces are a separate relationship, retrieved via cls.getInterfaces().

Why does getDeclaredMethod() find a private method, but invoke() on it throw IllegalAccessException?

Locating a member via reflection is independent of Java's language-level access control โ€” reflection can see a private method, but invoking it still respects private unless access checks are explicitly disabled with setAccessible(true).

What does setAccessible(true) actually do, and why is it dangerous?

It disables Java's language-level access checks at runtime, allowing invocation of private methods/fields/constructors. It's dangerous because it breaks encapsulation, can expose sensitive data, can violate class invariants, and can throw SecurityException in environments with a SecurityManager or reflection restrictions.

Why do primitive class literals like int.class exist?

Because int isn't a class, but reflective APIs like getDeclaredMethod() only accept Class<?> arguments to describe parameter types โ€” Java provides special JVM-backed Class objects for primitives so a primitive-typed parameter can still be described reflectively.

Why does printArray(int[]) behave differently from printArray(String[]) when printArray is declared as <T> void printArray(T... a)?

Generics require reference-type elements. int[] is itself a reference type, so Java infers T = int[] and the whole array collapses into a single varargs element. String[]'s elements are already reference types, so it behaves as expected โ€” one varargs element per array entry.

What role does reflection play in serialization libraries like Jackson?

At runtime, a generic serializer doesn't know a given object's fields, getters/setters, or annotations in advance โ€” reflection lets it inspect the class structure and read/write fields dynamically, which is exactly the "unknown until runtime" problem reflection exists to solve.

Why is compile-time classpath insufficient for deserialization?

Because deserialization happens at runtime, using the JVM's runtime classpath to locate and load the class by its fully qualified name from the serialized stream โ€” a class being compiled successfully says nothing about whether it's present in the deployed runtime environment.

What does Class.forName() do, and when would you use it?

It loads a class at runtime given its fully qualified class name, triggering class loading and static initialization โ€” used whenever the class to instantiate is only known at runtime, such as in frameworks, serializers, and plugin systems.

What's the difference between a Spring singleton and a JVM (classic) singleton?

A Spring singleton is scoped to one instance per Spring application context โ€” multiple containers in the same JVM can each hold their own instance. A classic JVM singleton is enforced at the class level and is one instance per JVM regardless of any container.

How does Spring resolve which bean to inject when autowiring by type?

Type is checked first; if multiple beans of a matching type exist, qualifiers or explicit bean names are used to disambiguate the choice.

How does Spring use reflection and graph algorithms together for dependency injection?

Reflection inspects constructors, fields, and annotated members (e.g. @Autowired) to discover what a bean needs. Separately, Spring builds a directed dependency graph of beans and uses a topological sort to determine a valid instantiation order, detect missing dependencies, and catch circular dependencies โ€” reflection handles the "what," the graph algorithm handles the "in what order."


โŒ If reflection can locate a private method, it can call it too.

โœ” Locating and invoking are separate checks โ€” Java still enforces access control on invoke() unless you explicitly call setAccessible(true).


โŒ Autoboxing for varargs/reflection calls happens at runtime, right before the method executes.

โœ” Autoboxing happens at compile time โ€” by the time the program runs, primitives passed to an Object...-style parameter are already boxed; runtime only ever sees objects, partly due to type erasure.


โŒ A generic varargs method like <T> void f(T... a) treats an int[] argument the same way it treats a String[] argument.

โœ” int[] is itself inferred as T, collapsing the whole array into one element โ€” because generics require the elements, not just the container, to be reference types. String[]'s elements already are reference types, so it behaves as expected.


โŒ If a class compiled successfully, deserialization involving it will always work at runtime.

โœ” Compile-time classpath and runtime classpath are different things โ€” a class missing from the runtime classpath causes ClassNotFoundException during deserialization regardless of whether it compiled fine originally.


โŒ A Spring singleton bean means there's exactly one instance of that class in the whole JVM.

โœ” It means one instance per Spring application context โ€” multiple containers in the same JVM can each hold a separate instance.


What You Should Remember Forever

Reflection            โ†’ inspect/act on types unknown at compile time
Class<T>               โ†’ every object's own metadata, reachable via getClass()

getDeclared*()          โ†’ this class only, any access level, no inheritance
get*() (no "Declared")  โ†’ public only, including inherited members

getDeclaredMethod finds โ†’ private methods
invoke() still enforces โ†’ access control, unless setAccessible(true)

setAccessible(true)    โ†’ disables access checks; breaks encapsulation;
                          framework territory, not business logic

int.class               โ†’ primitive class literal, needed because int
                          isn't itself a Class

Serialization           โ†’ object โ†’ transferable format (JSON/bytes),
                          powered by reflection to read structure dynamically
Deserialization         โ†’ needs the class on the RUNTIME classpath,
                          not just compiled successfully once

Spring Bean              โ†’ lifecycle owned by the container, not your code
Spring singleton         โ†’ one instance per container, not per JVM
Autowiring               โ†’ reflection discovers dependencies;
                            a dependency graph + topological sort
                            decides instantiation order

Stage 2 ยท Week 1 Checklist

  • [x] Explain what problem reflection solves and why it belongs at framework boundaries, not in business logic
  • [x] Explain the difference between getDeclared*() and non-declared reflective lookups
  • [x] Explain why getDeclaredMethod() can find a private method but invoke() still needs setAccessible(true)
  • [x] Explain why setAccessible(true) is dangerous, and when it throws SecurityException
  • [x] Explain why primitive class literals like int.class exist
  • [x] Explain why reflection is central to how serialization libraries like Jackson work
  • [x] List at least five things a senior engineer would guard against in a hand-rolled reflective serializer
  • [x] Explain the difference between compile-time and runtime classpath, and why it matters for deserialization
  • [x] Explain the difference between a Spring singleton and a JVM singleton
  • [x] Explain how Spring combines reflection with a dependency graph to perform autowiring