Encapsulation — Professional¶
What? Bytecode-level access checks (
getfield/putfield/invokevirtualand friends), JVMS access control, JPMS module enforcement at the bytecode level, the bytecode shape of records, and how the JIT eliminates encapsulation overhead. How? Read class files withjavap -v -p, watch JIT decisions with-XX:+PrintInlining, and understand the verifier's role in preventing access violations.
1. private in bytecode¶
A field declared private has access flag 0x0002 (ACC_PRIVATE). The verifier (JVMS §4.10.1.6) and the runtime (JVMS §5.4.4) enforce that only methods declared in the same class (and nested classes via synthetic accessors pre-Java 11, direct nestmate access since Java 11) can read/write it.
Access from outside via reflection is permitted only after setAccessible(true) (and may require --add-opens in JPMS).
2. Nestmates (Java 11+)¶
Before Java 11, accessing private members of an outer class from a nested class required synthetic accessor methods (access$000, etc.) generated by javac. These bloated bytecode and added indirection.
Since Java 11 (JEP 181), nest hosts and nest members are first-class concepts. The JVM allows direct access between nestmates without bridge methods.
Result: cleaner bytecode, slightly faster cold paths.
3. getfield and putfield opcodes¶
JVMS §6.5: - getfield: pop receiver, read field via constant pool ref, push value - putfield: pop receiver and value, write field
The verifier ensures the access is allowed (private → same class only; protected → subclass + same package; etc.).
A getter compiles to:
The JIT inlines this so direct field access is unchanged from a programmatic getter.
4. invokespecial for private methods¶
Pre-Java 11: private methods used invokespecial. Java 11 changes this to invokevirtual for private methods — a verifier change that simplified the model.
In either case, JIT performance is identical: private methods are non-virtual (no override possible), so dispatch is direct.
5. Records in bytecode¶
Compiles to roughly:
public final class Point extends java.lang.Record {
private final double x; // ACC_PRIVATE | ACC_FINAL
private final double y; // ACC_PRIVATE | ACC_FINAL
public Point(double, double); // canonical constructor
Code: aload_0 invokespecial Record.<init> aload_0 dload_1 putfield x ...
public double x(); // ACC_PUBLIC
Code: aload_0 getfield x dreturn
public double y(); // ACC_PUBLIC
Code: aload_0 getfield y dreturn
public boolean equals(Object); // synthetic, uses invokedynamic to ObjectMethods
public int hashCode(); // synthetic, uses invokedynamic
public String toString(); // synthetic, uses invokedynamic
}
The equals/hashCode/toString use invokedynamic with ObjectMethods.bootstrap for compactness and consistency.
The class also has a Record attribute listing its components, used by reflection (Class.getRecordComponents).
6. JPMS access checks¶
A module-info.class declares:
module com.example.banking {
exports com.example.banking.api;
// com.example.banking.internal is not exported
}
When code in another module performs new InternalClass(), the JVM at link time checks the module graph and throws IllegalAccessError if the package isn't exported.
Reflective access additionally requires opens (or runtime --add-opens).
The JIT inlines the access check, so steady-state cost is zero.
7. Sealed types and access¶
A sealed type's permits list creates an access constraint: only listed classes can extend. The verifier checks the PermittedSubclasses attribute (JVMS §4.7.31).
A non-permitted subclass produces IncompatibleClassChangeError at link time.
This is encapsulation of the hierarchy itself — only the listed classes can participate.
8. Frozen final fields and the JMM¶
Per JLS §17.5, a final field has a "freeze" action at the end of <init>. The JVM ensures any thread observing the constructed reference sees the field's correct value, without explicit synchronization.
This is critical for safe immutability: a record with private final fields is automatically thread-safe for sharing.
9. Synthetic accessors for inner classes¶
Pre-Java 11:
class Outer {
private int x;
class Inner { int read() { return x; } } // can't access private from outside class
}
Compiled to:
class Outer {
static int access$000(Outer o) { return o.x; } // synthetic accessor
}
class Outer$Inner {
int read() { return Outer.access$000(o); }
}
Java 11+ removes these via nestmate access. Resulting bytecode is smaller and easier to verify.
10. Reflection paths¶
Field.get(instance): - Checks access (private + not setAccessible(true) → IllegalAccessException) - Bridges through MethodHandle for performance after Java 7 - Subject to --add-opens in JPMS
MethodHandles.privateLookupIn(Class, lookup): - Java 9+ way to get a Lookup with full access to a target class's private members - Requires the source module to have opens to the target's module - Used by frameworks like Hibernate, ByteBuddy
11. Frameworks and encapsulation¶
Most major Java frameworks need access to private fields: - Hibernate / JPA: reads/writes @Entity fields directly - Jackson: reads private getters/fields for serialization - Spring: injects into private fields with @Autowired - Mockito: bypasses encapsulation to verify internal state
Trade-off: pragmatic vs strict. JPMS lets you choose: open packages for frameworks, keep others strict.
12. JIT inlining of accessors¶
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+PrintCompilation MyApp 2>&1 | grep accessor
Look for: - inline (hot) on accessors → JIT collapsed them - failed: not inlineable (no method body) → abstract or uncompiled
After warmup, well-designed accessors disappear in the optimized code.
13. The cost of volatile fields¶
Encapsulating a thread-safe primitive often involves volatile:
Each read/write of count includes a memory barrier (LoadLoad / StoreStore on x86; LoadStore / StoreStore on ARM). Cost: ~1-5 ns per access, no JIT optimization.
For high-throughput counters, prefer LongAdder or AtomicLong. For state read by many but written rarely, volatile is fine.
14. Where the spec says it¶
| Topic | Source |
|---|---|
| Access modifiers | JLS §6.6 |
| Class file access flags | JVMS §4.1, §4.5, §4.6 |
| Verifier access checks | JVMS §4.10.1.6 |
| Linker access checks | JVMS §5.4.4 |
| Module access (JPMS) | JLS §7.7, JEP 261 |
| Records | JLS §8.10 |
| Sealed types | JLS §8.1.1.2 |
| Final field semantics | JLS §17.5 |
| Nestmates | JEP 181 |
Memorize this: access modifiers are bytecode flags enforced by the verifier and linker. The JIT inlines accessors, making encapsulation free at runtime. Records, modules, and sealed types extend encapsulation declaratively. Reflection bypasses it (with consent). The JVM is your enforcer, not just javac.