Skip to content
Java reflection 7 min read

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: setAccessible does NOT change the field or method itself. It only affects this particular Field/Method object’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 final fields 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 final fields 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 why final fields 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 setAccessible on non-public members throws InaccessibleObjectException.
  • 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

SituationSolution
You own the target moduleAdd opens com.example.pkg; or opens com.example.pkg to your.module; in module-info.java
Third-party library on classpathPass --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-opens flag 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:

OperationRelative cost
Direct field read1× (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
  • 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 opens directive that controls whether deep reflection is permitted across module boundaries.
  • Encapsulation — The design principle that setAccessible bypasses; know what you are breaking before you break it.
  • Java Memory Model — Explains why mutating final fields 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.
Last updated June 13, 2026
Was this helpful?