Command Palette

Search for a command to run...

PHASE 15Advanced ~33 min· topic 2 of 6

Topic 15.2

Reflection

In one line

Reflection (java.lang.reflect) lets a running program examine classes it was never compiled against: list their fields, methods and constructors, create objects, call methods and read fields by name. It powers frameworks, serializers and test runners, at the cost of speed, compile-time safety and encapsulation, and since Java 16 the module system blocks it from reaching into JDK internals.

Think of it like this

Normally you use a TV with its remote: you press the buttons the maker gave you. Reflection is like unscrewing the back panel and reading the labels on the circuit board: you can see every part, even the ones the maker never meant you to touch, and with the right tool you can poke them directly. It's powerful for a repair technician (a framework), risky for everyday use, and some newer TVs have sealed screws (the module system) that stop you opening them at all.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Reflection
A running program looking at, and using, its own classes, methods and fields as data.
Class object
The single object of type Class that the JVM creates for each loaded class. It describes the class's members.
Member
A field, method or constructor of a class.
Declared member
A member written in that class itself, not inherited from a parent class.
invoke
Calling a method through its Method object instead of writing the call in the code.
setAccessible
A switch on a Field, Method or Constructor object that turns off Java's private/protected checks for reflective use.
Strong encapsulation
The module system's rule that code outside a module can't reach its non-exported or non-opened packages, even with reflection.
Dynamic proxy
An object created at runtime that implements chosen interfaces and sends every method call to one handler method.

Step by step

01Getting a Class object

Class<Product> c = Product.class; is checked by the compiler. obj.getClass() returns the object's real runtime class, which may be a subclass of the variable's type. Class.forName("Product") loads and initializes the class by its fully-qualified name; frameworks use it to load classes named in configuration files.

There is exactly one Class object per class per class loader, so a.getClass() == b.getClass() is a valid identity test. Primitives have Class objects too (int.class), and so do arrays (int[].class).

Main.javawhole filejava
Class<?> a = String.class;                     // class literal
Class<?> b = "hello".getClass();               // from an object
Class<?> c = Class.forName("java.lang.String"); // by name, at runtime
System.out.println(a == b && b == c);          // true: one Class object per class

02Listing members

getDeclaredFields() returns Field[] with every field the class itself declares. Each Field knows its name, type (getType()), full generic type (getGenericType(), which still says List<String> because declarations keep their generic signatures even though objects don't, Topic 8.6) and modifiers (getModifiers() returns an int bit set; Modifier.toString turns it into words).

The compiler sometimes adds members you didn't write: bridge methods for generics and covariant returns, values()/valueOf on enums, this$0 fields in inner classes, and lambda$main$0 methods for lambdas. isSynthetic() and isBridge() let you filter them out.

Listing membersdiagram
Rendering diagram…

03Creating objects and calling methods

c.getDeclaredConstructor(String.class, long.class) finds a constructor by its exact parameter types; newInstance("Kettle", 2000L) calls it. The old Class.newInstance() is deprecated since Java 9 because it rethrew checked exceptions without declaring them.

getMethod("getName") finds a public method (searching superclasses too); getDeclaredMethod("applyDiscount", int.class) finds any method declared in that class. Parameter types must match exactly: asking for int.class won't find a method taking long, and you get NoSuchMethodException.

Main.javawhole filejava
Constructor<Product> ctor = Product.class.getDeclaredConstructor(String.class, long.class);
Product p = ctor.newInstance("Kettle", 2000L);
Method m = Product.class.getMethod("getName");
Object result = m.invoke(p);           // "Kettle", returned as Object

04Exceptions are wrapped

If the method you invoke throws, reflection catches it and throws InvocationTargetException instead, with the original as its cause. This is why stack traces from frameworks often show Caused by: followed by your real error. Always unwrap with e.getCause().

Other reflective failures have their own types: NoSuchMethodException/NoSuchFieldException (wrong name or parameter types), IllegalAccessException (access check failed), IllegalArgumentException (wrong argument types or count, for example argument type mismatch).

05setAccessible and the module wall

field.setAccessible(true) lets you read a private field of a class in your own code base. That's how Jackson fills private fields and how JUnit calls package-private test methods.

Since Java 9 there's a second check: the target's module must open the package to you. Your classes on the classpath live in the unnamed module, which is open to everyone, so it works there. JDK packages like java.lang are not opened, so String.class.getDeclaredField("value").setAccessible(true) throws InaccessibleObjectException. trySetAccessible() returns false instead of throwing.

The command-line flag --add-opens java.base/java.lang=ALL-UNNAMED opens a package anyway. Old libraries needed it; seeing it in a launch script is a sign the library depends on JDK internals and may break on upgrades.

terminal
$ java --add-opens java.base/java.lang=ALL-UNNAMED -jar legacy-app.jar
── expected output ──
(starts normally: java.lang is now open to classpath code for deep reflection)

06Dynamic proxies

Proxy.newProxyInstance(loader, new Class<?>[]{ Service.class }, handler) creates, at runtime, an object that implements Service. Every call on it goes to handler.invoke(proxy, method, args), where you can log, time, check permissions or forward the call to a real object.

This is the Proxy pattern (System Design course, design patterns) built into the JDK. Spring uses it for @Transactional on interfaces, and Mockito-style libraries use similar ideas. JDK proxies only work for interfaces; for classes, libraries generate subclasses with bytecode tools such as ByteBuddy.

Dynamic proxiesdiagram
Rendering diagram…

07What reflection costs

A direct call is a single bytecode the JIT can inline. A reflective call boxes arguments into an Object[], checks access (unless setAccessible(true) was set), checks argument types, and wraps exceptions. Since Java 18 (JEP 416), core reflection is built on method handles, and a Method stored in a static final field can be optimized well; looking it up again on every call is the expensive mistake.

Beyond speed: the compiler can't check names or types, IDEs can't find usages, and private fields stop being private. Reflection also defeats tools like GraalVM native image unless you list the reflected classes in configuration.

Try it yourself

  1. 1

    Spot the synthetic members

    In "Inspecting a class", add public Runnable printer() { return () -> System.out.println(name); } to Product. Predict the new method list, then run. A lambda$printer$0 method appears: the compiler turned the lambda body into a private synthetic method. Filter it out with if (m.isSynthetic()) continue;.

  2. 2

    Look up the wrong overload

    In "Creating, invoking and touching private state", change getDeclaredMethod("applyDiscount", int.class) to long.class. Predict the exception. You get java.lang.NoSuchMethodException: Product.applyDiscount(long): parameter types must match exactly; no widening happens at lookup.

  3. 3

    Let the wrapper escape

    In the proxy example, replace throw e.getCause(); with throw e;. Run it: the caller's catch (IllegalArgumentException e) no longer matches, and the program dies with java.lang.reflect.UndeclaredThrowableException, because the interface method doesn't declare the checked InvocationTargetException.

Code & diagrams

Inspecting a class: fields, methods, constructors Java 8+ New tab

getGenericType still knows List<String>: generic signatures of declarations survive erasure, only objects lose their type arguments.

Sign in to run this example in your browser.

Expected output

class Product extends Object implements Priced
field: public static final java.lang.String CURRENCY
field: private final java.lang.String name
field: private long price
field: protected java.util.List<java.lang.String> tags
method: private void applyDiscount(int)
method: public String getName()
method: public long price()
constructor: public Product()
constructor: public Product(String, long)
Creating, invoking and touching private state Java 5+ New tab
Sign in to run this example in your browser.

Expected output

getName() -> Kettle
blocked: class Main cannot access a member of class Product with modifiers "private"
price after 25% off: 1500
wrapped: java.lang.IllegalArgumentException: discount over 100%: 150
The module wall around JDK internals Java 9+ New tab

The real message ends with "to unnamed module @<hash>"; the hash changes every run, so the example cuts it off.

Sign in to run this example in your browser.

Expected output

found: value of type byte[]
trySetAccessible: false
InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang"
my own class is in a named module: false
String's module: java.base
java.lang open to me: false
A logging dynamic proxy Java 8+ New tab

Rethrowing e.getCause() matters: if the handler let InvocationTargetException escape, the caller would get an UndeclaredThrowableException instead.

Sign in to run this example in your browser.

Expected output

-> pay[A-17, 250]
<- paid 250 for A-17
-> pay[A-18, -5]
!! amount must be positive
caller saw: IllegalArgumentException
proxy is a PaymentService: true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call a private method without setAccessible

Call getDeclaredMethod("applyDiscount", int.class).invoke(p, 25) from Main without calling setAccessible(true) first, and don't catch the exception.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.IllegalAccessException: class Main cannot access a member of class Product with modifiers "private"

Break #2

Reach into the JDK

Call String.class.getDeclaredField("value").setAccessible(true);.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @2ddc8ecb

Break #3

Change a record's final field

Get a record component's field (record P(int x)), call setAccessible(true), then f.setInt(new P(1), 5).

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.IllegalAccessException: Can not set final int field P.x to (int)5

Myth vs fact

Myth

private means nothing outside the class can ever touch it.

Fact

In your own classpath code, setAccessible(true) bypasses private. Only the module system (for named modules that don't open a package) truly enforces it.

Myth

getMethods() returns all methods of a class.

Fact

getMethods() returns public methods including inherited ones; getDeclaredMethods() returns all methods declared in that class itself, of any visibility, but no inherited ones.

Myth

Reflection is always too slow to use.

Fact

A cached Method or MethodHandle called many times is fast enough for most framework work; the slow part is repeated lookup and the lost compile-time checks, not every single call.

Myth

getDeclaredMethods returns methods in source order.

Fact

The order is unspecified and differs between JVMs and runs. Sort the array if order matters.

When it breaks

A library upgrade to Java 17 fails at startup with InaccessibleObjectException.

What you see

The service won't start; logs show Unable to make field ... accessible: module java.base does not "opens java.util". Old versions of serialization, mocking and bytecode libraries reflected into JDK internals.

Fix & prevent

Upgrade the library to a version that supports strong encapsulation. As a stop-gap only, add the exact --add-opens the message names. Track every --add-opens in the launch script as technical debt.

A method is renamed in a refactor, and a reflective call by name breaks in production.

What you see

Compilation and unit tests pass; at runtime NoSuchMethodException appears only on the code path that uses getMethod("oldName").

Fix & prevent

Avoid string-based lookups in application code; use interfaces or method references. Where reflection is necessary, resolve all names eagerly at startup and fail fast, and cover the path with a test.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    JEP 416 (Java 18) reimplemented Method.invoke, Constructor.newInstance and Field access on top of java.lang.invoke method handles, replacing the old scheme that generated bytecode accessor classes after 15 calls (the "inflation threshold"). A Method held in a static final field lets the JIT treat the handle as a constant and inline through it.

  • ▸

    MethodHandles.lookup().findVirtual(Product.class, "getName", MethodType.methodType(String.class)) gives a MethodHandle with access checked once, at lookup. privateLookupIn(cls, lookup) (Java 9) is the module-aware way to get deep access, and it respects opens declarations. VarHandle (Java 9) gives field access with memory-ordering modes (Topic 13.4).

  • ▸

    getDeclaredFields on a record returns the private final component fields; getRecordComponents() (Java 16) returns RecordComponents with their accessors, which is how serialization libraries handle records without setters. isSealed() and getPermittedSubclasses() (Java 17) expose sealed hierarchies.

  • ▸

    Every getDeclaredMethods() call copies the cached array, and getMethod performs a search; frameworks build their own caches (ClassValue<T> is the JDK tool for per-class caches that don't leak class loaders). Reflection metadata lives in metaspace, and Class.forName with the wrong class loader is a classic cause of ClassNotFoundException in application servers.

Remember this

  1. 1

    Every loaded class has exactly one **Class object** describing it. You get it three ways: Product.class (a class literal, known at compile time), obj.getClass() (the runtime class of an object) or Class.forName("com.shop.Product") (by name, at runtime, which may throw ClassNotFoundException). From the Class you reach Field, Method and Constructor objects, plus the superclass, interfaces, modifiers and annotations.

  2. 2

    There are two families of lookup methods. **getFields(), getMethods(), getConstructors() return only public members, including inherited ones. getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors() return every member declared in that class itself, of any visibility, but nothing inherited. The order of the returned arrays is not specified**, so sort them if you print them.

  3. 3

    With a member in hand you can act: constructor.newInstance(args) creates an object, method.invoke(target, args) calls a method (pass null as the target for a static method), and field.get(obj) / field.set(obj, value) read and write a field. Arguments and results are Objects, so primitives are boxed. If the called method throws, you get an **InvocationTargetException** whose getCause() is the real exception.

  4. 4

    Access rules still apply: touching a private member from another class throws IllegalAccessException unless you first call **setAccessible(true)**, which switches off the language access check for that one Field/Method/Constructor object. Since Java 9 the module system adds a second wall: setAccessible on a member of a package that its module doesn't open to you throws InaccessibleObjectException. Java 16 made this strong encapsulation the default for the JDK (JEP 396), and Java 17 removed the --illegal-access escape hatch (JEP 403).

  5. 5

    Reflection has costs. A reflective call does access checks, boxes arguments into an Object[], and wraps exceptions; it's slower than a direct call, especially when the Method lookup is repeated each time (lookups scan and copy member arrays). Errors that the compiler would catch, like a misspelled method name or wrong argument type, become runtime exceptions. Refactoring tools can't see string-based calls, so renaming a method silently breaks getMethod("oldName").

  6. 6

    Use reflection for framework and tooling code: dependency injection, serialization (Jackson, Gson), ORMs (Hibernate), test runners (JUnit), plug-in loading, and debuggers. In application code, prefer interfaces and polymorphism. Related, faster APIs exist: **MethodHandle (Java 7) and VarHandle** (Java 9) in java.lang.invoke, and dynamic proxies (java.lang.reflect.Proxy) that implement interfaces at runtime.

Explain it without notes

01

What is reflection, and where is it used in real systems?

02

What is the difference between getMethods() and getDeclaredMethods()?

03

Why does a reflective call throw InvocationTargetException, and how do you handle it?

04

How did the module system change what setAccessible(true) can do?

05

What are the costs of reflection, and what are the alternatives?

Practice

01

Write toMap(Object o) that returns a TreeMap of every declared field name to its value, using reflection, and print it for an object with three private fields.

02

Call a static method Math.max(int, int) through reflection and print the result.

03

Write a method that copies every non-static field from one object to another of the same class, and show it working.

Trade-offs

  • ↔

    Reflection makes generic frameworks possible (one serializer for every class) but moves errors from compile time to runtime.

  • ↔

    setAccessible gives access to private state that frameworks need, but breaks the encapsulation that keeps classes safe to change.

  • ↔

    Runtime reflection needs no build step; build-time code generation is faster at startup and checked by the compiler, but adds generated code and tooling.

Done when you can

  • Done when you can get a Class object three ways and list its declared fields, methods and constructors in a stable order.

  • Done when you can create objects, invoke methods and read fields reflectively, and unwrap InvocationTargetException.

  • Done when you can explain getMethods versus getDeclaredMethods.

  • Done when you can explain why setAccessible fails on JDK internals since Java 16/17 and what --add-opens does.

  • Done when you can build a dynamic proxy and name the costs of reflection and its alternatives.