Accessing Private Members
Java’s Reflection API can do something that ordinary code cannot: reach past private access modifiers and interact with fields and methods that are normally hidden from the outside world. This is how testing libraries, serialization frameworks, and dependency-injection containers do their magic — and understanding it helps you use those tools confidently and avoid the pitfalls.
Why Would You Ever Do This?
Your day-to-day application code should respect encapsulation. But some legitimate use cases require direct access to private members:
- Unit testing — verifying the internal state of an object without adding getters just for tests
- Serialization/deserialization — frameworks like Jackson and Gson read private fields to convert objects to and from JSON
- Mocking and spying — tools like Mockito inject behavior into private fields
- Legacy migration — reading private data from a poorly designed third-party class you cannot modify
Warning: Using
setAccessible(true)in production application code couples you to implementation details that can change with any library update. Treat it as a last resort, not a convenience.
The Key Method: setAccessible(true)
Every Field, Method, and Constructor object extends AccessibleObject, which exposes a single gate-keeping method:
accessibleObject.setAccessible(true);
Calling this with true suppresses the normal Java access check for that reflective operation. Once set, you can read or write a private field or invoke a private method as if the access modifier were not there.
Note:
setAccessibledoes NOT change the field or method itself. It only affects this particularField/Methodobject’s access check. The class’s source code and bytecode are untouched.
Accessing a Private Field
Here is the complete recipe: get the Field, make it accessible, then get or set its value.
import java.lang.reflect.Field;
class BankAccount {
private double balance = 1000.00;
}
public class FieldAccessDemo {
public static void main(String[] args) throws Exception {
BankAccount account = new BankAccount();
// Step 1: get the Field descriptor for "balance"
Field balanceField = BankAccount.class.getDeclaredField("balance");
// Step 2: suppress access check
balanceField.setAccessible(true);
// Step 3: read the current value
double current = (double) balanceField.get(account);
System.out.println("Balance before: " + current);
// Step 4: write a new value
balanceField.set(account, 2500.00);
System.out.println("Balance after: " + (double) balanceField.get(account));
}
}
Output:
Balance before: 1000.0
Balance after: 2500.0
Use getDeclaredField(name) (not getField(name)) — the getDeclaredXxx family returns members of all access levels declared directly on the class, while getXxx returns only public members, including inherited ones.
Accessing a Private Method
The process mirrors fields: retrieve a Method via getDeclaredMethod, call setAccessible(true), then call invoke.
import java.lang.reflect.Method;
class Vault {
private String openDoor(String code) {
return code.equals("1234") ? "OPEN" : "LOCKED";
}
}
public class MethodAccessDemo {
public static void main(String[] args) throws Exception {
Vault vault = new Vault();
// getDeclaredMethod(name, parameterTypes...)
Method openDoor = Vault.class.getDeclaredMethod("openDoor", String.class);
openDoor.setAccessible(true);
// invoke(instance, arguments...)
String result = (String) openDoor.invoke(vault, "1234");
System.out.println("Door status: " + result);
result = (String) openDoor.invoke(vault, "0000");
System.out.println("Door status: " + result);
}
}
Output:
Door status: OPEN
Door status: LOCKED
For static private methods, pass null as the first argument to invoke since there is no instance to bind to.
Accessing a Private Constructor
You can even instantiate classes that have only private constructors (the Singleton pattern is a classic example):
import java.lang.reflect.Constructor;
class Singleton {
private static final Singleton INSTANCE = new Singleton();
private int value = 42;
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
public class ConstructorAccessDemo {
public static void main(String[] args) throws Exception {
Constructor<Singleton> ctor =
Singleton.class.getDeclaredConstructor(); // no params
ctor.setAccessible(true);
Singleton second = ctor.newInstance();
Field valueField = Singleton.class.getDeclaredField("value");
valueField.setAccessible(true);
System.out.println("Value: " + valueField.get(second));
}
}
Output:
Value: 42
Tip: This technique shows why a Singleton backed only by a private constructor is not truly unbreakable. If you need a genuine singleton that cannot be duplicated by reflection, use an enum — the JVM itself enforces single instantiation for enum constants.
Dealing with final Fields
You can call setAccessible(true) on a final field and even call set() on it. Whether that change actually takes effect is nuanced:
- For instance
finalfields set in the constructor, the write may visibly succeed. - For static final fields (constants), the JIT often inlines the value at compile time, so the “change” may not be seen by code that was compiled before the reflective write.
import java.lang.reflect.Field;
class Config {
private final String env = "production";
}
public class FinalFieldDemo {
public static void main(String[] args) throws Exception {
Config cfg = new Config();
Field envField = Config.class.getDeclaredField("env");
envField.setAccessible(true);
System.out.println("Before: " + envField.get(cfg));
envField.set(cfg, "staging");
System.out.println("After: " + envField.get(cfg));
}
}
Output:
Before: production
After: staging
Warning: Mutating
finalfields breaks the Java Memory Model’s guarantees and can produce unpredictable results in multi-threaded code. Avoid this in any real system. See Java Memory Model for whyfinalfields carry special visibility semantics.
Java 9+ Module System Restrictions
Before Java 9, setAccessible(true) almost always worked. The Java 9 module system changed that. When your code lives in one module and the target class lives in another, the runtime now enforces strong encapsulation:
- If the target package is exported but not opened, you can read public members but
setAccessibleon non-public members throwsInaccessibleObjectException. - If the target package is opened (either unconditionally or to your specific module), deep reflection is allowed.
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
Common workarounds
| Situation | Solution |
|---|---|
| You own the target module | Add opens com.example.pkg; or opens com.example.pkg to your.module; in module-info.java |
| Third-party library on classpath | Pass --add-opens java.base/java.lang=ALL-UNNAMED to the JVM on startup |
| Framework compatibility (e.g., Spring, Hibernate) | Frameworks often require --add-opens flags; check their migration guides |
Note: The
--add-opensflag is a command-line escape hatch, not a long-term solution. If you maintain a library, prefer proper public APIs.
Under the Hood
How Access Checks Work
When you call field.get(obj) without setAccessible(true), the JVM calls Field.checkAccess(), which compares the caller’s class and module against the field’s declared class and module. This check happens via native code inside the JVM and consults the module graph built at startup.
Calling setAccessible(true) sets a flag on the AccessibleObject that tells the JVM to skip that check on subsequent get/set/invoke calls. It does not modify the class file or any module descriptor — it is purely a runtime override for that particular reflective handle.
Performance Implications
Reflective access is slower than direct field access, but the gap narrows after JIT warm-up:
| Operation | Relative cost |
|---|---|
| Direct field read | 1× (baseline) |
field.get() — first 15 calls | ~10–20× |
field.get() — after JIT inflation | ~2–4× |
The JVM uses an “inflation” strategy: the first 15 reflective invocations go through a slow interpreter path; after that, the runtime generates native accessor code and caches it. The threshold is controlled by the JVM flag -Dsun.reflect.inflationThreshold (default 15).
For hot paths that require dynamic field access, consider java.lang.invoke.VarHandle (introduced in Java 9) or MethodHandle, which compile down to near-direct performance because the JIT understands them natively.
AccessibleObject.canAccess() (Java 9+)
Since Java 9, you can check whether access would succeed before trying it:
Field f = SomeClass.class.getDeclaredField("secret");
if (f.canAccess(instance)) {
// already accessible without setAccessible
System.out.println(f.get(instance));
} else {
f.setAccessible(true); // may throw InaccessibleObjectException
System.out.println(f.get(instance));
}
This lets you write defensive code that handles both accessible and inaccessible cases gracefully.
Practical Example: A Simple Object Inspector
Here is a small utility that prints every field and its value for any object — the kind of thing a debugger or logging framework might do internally:
import java.lang.reflect.*;
public class ObjectInspector {
public static void inspect(Object obj) throws Exception {
Class<?> cls = obj.getClass();
System.out.println("=== " + cls.getSimpleName() + " ===");
for (Field field : cls.getDeclaredFields()) {
field.setAccessible(true);
System.out.printf(" %-20s = %s%n",
field.getName(), field.get(obj));
}
}
public static void main(String[] args) throws Exception {
inspect(new java.awt.Point(3, 7));
}
}
Output:
=== Point ===
x = 3
y = 7
Related Topics
- Reflection API — The complete overview of Java Reflection: inspecting classes, listing methods, and invoking them dynamically.
- Creating Objects Reflectively — Use
Constructor.newInstance()to instantiate classes at runtime from a string class name. - Java 9: Modules (JPMS) — Understand the
opensdirective that controls whether deep reflection is permitted across module boundaries. - Encapsulation — The design principle that
setAccessiblebypasses; know what you are breaking before you break it. - Java Memory Model — Explains why mutating
finalfields via reflection is dangerous in concurrent code. - Annotations — Reflection and annotations work hand-in-hand; frameworks read runtime-retained annotations on private members to drive behavior.