reflection
Class, Field, Method, and why setAccessible stopped working.
- chapter
- chapter 11.01
- reading time
- 8 min read
every loaded class has a Class object, and it will hand you Field, Method and Constructor objects for everything the class declares. with those you can read fields, call methods and create objects by name, at runtime, with no compile-time knowledge of the class. that's reflection.
the catch since java 17: the JDK's own private parts are sealed off, and setAccessible(true) throws instead of shrugging.
the picture
you know how a framework like Spring can call your constructor, or JUnit can find your @Test methods, when neither of them has ever seen your code? they ask the class about itself.
think of a building with a directory at the front desk. the directory (the Class object) lists every room (fields), every service on offer (methods) and how to add a new floor (constructors). reflection is reading that directory and then walking into the rooms it lists. the directory is always there, because the JVM needed it to load the class in the first place.
there are three ways to get the directory:
Class<?> a = Dog.class; // you know the type at compile time
Class<?> b = someDog.getClass(); // you have an object; this is its runtime class
Class<?> c = Class.forName("Dog"); // you have a string (a config file, a plugin name)all three give the very same object: one Class per class per class loader. then you ask it for members. the API comes in pairs, and the pairs mean different things:
public methods all the way up: bark, eat, and Object's nine public methods. 11 in total. the package-private count() and Object's protected clone() are left out.
getX means public, inherited included. getDeclaredX means everything written in this class, private included, nothing inherited. the counts are what jdk 25 returns for these exact classes.so "give me every field, private and inherited" is a loop: getDeclaredFields() on the class, then on getSuperclass(), until you hit Object. every serialization library has that loop somewhere.
class Dog {
public void bark() {}
}
// Dog.class.getMethods().length == ?calling things by name
here's the whole toolkit in one go: find a class from a string, make an instance, call a method, poke a private field, and see what happens when the method you call throws.
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) throws Exception {
Class<?> c = Class.forName("Dog");
Object dog = c.getDeclaredConstructor().newInstance();
Method bark = c.getMethod("bark", int.class);
System.out.println(bark.invoke(dog, 2));
Field age = c.getDeclaredField("age");
age.setAccessible(true);
age.set(dog, 4);
System.out.println("age = " + age.get(dog));
try {
c.getMethod("bite").invoke(dog);
} catch (InvocationTargetException e) {
System.out.println("wrapped: " + e.getCause());
}
}
}
class Dog {
private int age = 3;
public String bark(int times) { return "woof ".repeat(times).trim(); }
public void bite() { throw new IllegalStateException("good dogs don't bite"); }
}woof woof age = 4 wrapped: java.lang.IllegalStateException: good dogs don't bite
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
things to notice:
- methods are found by name and parameter types.
getMethod("bark", int.class), because overloads share a name. get either one wrong and it's aNoSuchMethodExceptionat runtime, not a compile error. - everything is
Object.invoketakesObject...and returnsObject, so the2got boxed into anIntegerand unboxed again on the way in. the compiler checks nothing. getDeclaredConstructor(), notgetConstructor().Dogisn't public, so its default constructor isn't either, andgetConstructor()(public only) throwsNoSuchMethodException: Dog.<init>(). that one catches everybody once.- the method's own exception comes back wrapped in an
InvocationTargetException. the real one isgetCause().
setAccessible, and the wall
setAccessible(true) turns off the language's access checks for one Field, Method or Constructor. for your own classes it still works exactly like it always did. for the JDK's, it used to work too, and half the ecosystem quietly depended on reading String.value or calling ClassLoader.defineClass.
then modules arrived (modules). a module can export a package (you may use its public API) and separately open it (you may reflect into its privates). java.base exports java.lang and opens nothing.
the JDK didn't slam the door at once. java 9 to 15 let illegal reflective access through with a warning. java 16 denied it by default (JEP 396). java 17 removed the switch that turned it back on (JEP 403). here's where things stand on jdk 25:
java.base exports java.lang (you can use its public API) but doesn't open it (you can't break into its privates). since java 17 there is no global switch to turn this off.
the fixes, from best to worst:
- use the public API. most internals people reached for now have a real replacement:
MethodHandles.Lookup.defineClassinstead ofClassLoader.defineClass, VarHandles instead ofUnsafefield tricks, the FFM API instead ofUnsafememory. - open your own packages on purpose.
opens com.shop.model to com.fasterxml.jackson.databind;in yourmodule-info.javalets one library reflect into one package. --add-opens java.base/java.lang=ALL-UNNAMEDon the command line (orAdd-Opens:in a jar manifest). it works, and it's an honest label: this app breaks into the JDK.
the same tightening is coming for final. on 25, Field.set on a non-static final instance field still succeeds silently. java 26 prints a warning for it (JEP 500), and a later release will make it throw unless you pass --enable-final-field-mutation.
what it actually costs
"reflection is slow" is folklore with a kernel of truth. here's t.add(x) called six ways, measured with JMH:
Method in a static final costs about one extra nanosecond. looking the method up on every call is the real mistake.since java 18, Method.invoke is built on method handles (JEP 416). when the Method sits in a static final field, the JIT treats it as a constant and can inline almost straight through to the target. in an ordinary field it can't, and you pay for the argument array, the boxing and the checks on every call. getMethod itself searches the class and hands back a fresh copy of the Method every time, which is why it's the slowest row by far.
so the rule is boring: look things up once, keep them in static final fields, and the speed mostly stops mattering. what doesn't go away is everything else you lose. the compiler can't check names or types, refactoring tools can't see the calls, and ahead-of-time compilers like GraalVM native image need a config file listing every reflective access. that's why newer frameworks (Micronaut, Quarkus, Dagger) do their wiring at compile time with annotation processors instead.
what about generics, records and annotations?+
generic type arguments are erased from objects, but not from declarations. Field.getGenericType() on a List<String> names field returns a ParameterizedType that knows about String, which is how Jackson knows what to put in your list. the full story is in generics and reflection.
records get their own accessors: getRecordComponents() returns the components in declaration order, each with its accessor Method, which serialization libraries use instead of guessing at fields. and every Field, Method and Class can be asked for its runtime-visible annotations, which is the other half of how frameworks find your code.
- one
Classobject per loaded class. it hands outField,MethodandConstructorobjects. getXis public and inherited.getDeclaredXis everything in this class, nothing inherited.invokeboxes everything, checks nothing at compile time, and wraps thrown exceptions inInvocationTargetException.- since java 17, the JDK's privates are closed.
--add-opensis the explicit, honest escape hatch. - a
static finalMethodcosts about a nanosecond. a lookup on every call costs fourteen.