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).
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 class02Listing 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.
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.
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 Object04Exceptions 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.
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.
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
Spot the synthetic members
In "Inspecting a class", add
public Runnable printer() { return () -> System.out.println(name); }toProduct. Predict the new method list, then run. Alambda$printer$0method appears: the compiler turned the lambda body into a private synthetic method. Filter it out withif (m.isSynthetic()) continue;. - 2
Look up the wrong overload
In "Creating, invoking and touching private state", change
getDeclaredMethod("applyDiscount", int.class)tolong.class. Predict the exception. You getjava.lang.NoSuchMethodException: Product.applyDiscount(long): parameter types must match exactly; no widening happens at lookup. - 3
Let the wrapper escape
In the proxy example, replace
throw e.getCause();withthrow e;. Run it: the caller'scatch (IllegalArgumentException e)no longer matches, and the program dies withjava.lang.reflect.UndeclaredThrowableException, because the interface method doesn't declare the checkedInvocationTargetException.
Code & diagrams
getGenericType still knows List<String>: generic signatures of declarations survive erasure, only objects lose their type arguments.
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)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%: 150The real message ends with "to unnamed module @<hash>"; the hash changes every run, so the example cuts it off.
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: falseRethrowing e.getCause() matters: if the handler let InvocationTargetException escape, the caller would get an UndeclaredThrowableException instead.
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: trueBreak 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.
Break #2
Reach into the JDK
Call String.class.getDeclaredField("value").setAccessible(true);.
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).
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.newInstanceandFieldaccess on top ofjava.lang.invokemethod handles, replacing the old scheme that generated bytecode accessor classes after 15 calls (the "inflation threshold"). AMethodheld in astatic finalfield lets the JIT treat the handle as a constant and inline through it. - ▸
MethodHandles.lookup().findVirtual(Product.class, "getName", MethodType.methodType(String.class))gives aMethodHandlewith access checked once, at lookup.privateLookupIn(cls, lookup)(Java 9) is the module-aware way to get deep access, and it respectsopensdeclarations.VarHandle(Java 9) gives field access with memory-ordering modes (Topic 13.4). - ▸
getDeclaredFieldson a record returns the private final component fields;getRecordComponents()(Java 16) returnsRecordComponents with their accessors, which is how serialization libraries handle records without setters.isSealed()andgetPermittedSubclasses()(Java 17) expose sealed hierarchies. - ▸
Every
getDeclaredMethods()call copies the cached array, andgetMethodperforms 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, andClass.forNamewith the wrong class loader is a classic cause ofClassNotFoundExceptionin application servers.
Remember this
- 1
Every loaded class has exactly one **
Classobject** describing it. You get it three ways:Product.class(a class literal, known at compile time),obj.getClass()(the runtime class of an object) orClass.forName("com.shop.Product")(by name, at runtime, which may throwClassNotFoundException). From theClassyou reachField,MethodandConstructorobjects, plus the superclass, interfaces, modifiers and annotations. - 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
With a member in hand you can act:
constructor.newInstance(args)creates an object,method.invoke(target, args)calls a method (passnullas the target for a static method), andfield.get(obj)/field.set(obj, value)read and write a field. Arguments and results areObjects, so primitives are boxed. If the called method throws, you get an **InvocationTargetException** whosegetCause()is the real exception. - 4
Access rules still apply: touching a
privatemember from another class throwsIllegalAccessExceptionunless you first call **setAccessible(true)**, which switches off the language access check for that oneField/Method/Constructorobject. Since Java 9 the module system adds a second wall:setAccessibleon a member of a package that its module doesn't open to you throwsInaccessibleObjectException. Java 16 made this strong encapsulation the default for the JDK (JEP 396), and Java 17 removed the--illegal-accessescape hatch (JEP 403). - 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 theMethodlookup 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 breaksgetMethod("oldName"). - 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) andVarHandle** (Java 9) injava.lang.invoke, and dynamic proxies (java.lang.reflect.Proxy) that implement interfaces at runtime.
Explain it without notes
What is reflection, and where is it used in real systems?
What is the difference between getMethods() and getDeclaredMethods()?
Why does a reflective call throw InvocationTargetException, and how do you handle it?
How did the module system change what setAccessible(true) can do?
What are the costs of reflection, and what are the alternatives?
Practice
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.
Call a static method Math.max(int, int) through reflection and print the result.
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.