Last updated: 2026-09-26
Advanced Java: The JVM, Invariants, and Clean Structure
Students arriving from an imperative module often treat objects as magic containers that simply work. This page removes the magic in three layers: what the machine actually does with your objects (the JVM), how to make objects that cannot be broken (defensive programming), and how to keep systems of objects from congealing into a God class (decomposition).
1. The Operational Reality of the JVM
The stack and the heap
MyObject obj = new MyObject(); // what ACTUALLY happens?
Two allocations in one line, in two different places. new MyObject() allocates the object itself on the heap — the shared, garbage-collected pool. The variable obj lives in the current method's stack frame and holds only a reference — effectively an address. This is why Java is neither "pass by reference" nor naively "pass by value": it is pass-by-value where the value is a reference, as the Java Language Specification defines parameter passing1. Method calls copy the reference, so callee and caller see the same object — but reassigning the parameter inside the method changes nothing outside:reassigning inside a method wont stick
void mutate(List<String> list) { list.add("seen by caller"); } // same object
void reassign(List<String> list) { list = new ArrayList<>(); } // caller unaffected
Garbage collection as a system layer
The JVM frees an object when it is unreachable — no chain of references from any live thread's stack, static fields, or JNI roots leads to it. Two practical corollaries: "memory leaks" in Java are reachability leaks (a forgotten reference in a static map or listener list pins whole object graphs), and collection is not destruction on a schedule — finalisers may run late or never, which is why resources (files, sockets) are closed with try-with-resources, not left to the collector.
try-with-resources, not by leaving them to the collector.Bytecode and portability
"Write once, run anywhere" is not marketing; it is an architecture. javac does not emit machine code — it targets a virtual instruction set (bytecode in .class files). At runtime the JVM interprets that bytecode, profiles it, and JIT-compiles the hot paths to native machine code for whatever CPU it finds itself on. You can watch the layers: javap -c MyObject disassembles the bytecode your source became (and see CLI First for why running these tools by hand matters).
2. Defensive Programming & Strong Invariants
Encapsulation is invariant-protection, not field-hiding
Making fields private is the mechanism, not the point. The point is that some statements about an object must be always true — invariants — and the object's methods are the only code trusted to preserve them:same withdraw example as hoare triples
public final class BankAccount {
private long balancePence; // invariant: >= 0
public void withdraw(long pence) {
if (pence <= 0) throw new IllegalArgumentException("amount must be positive");
if (pence > balancePence) throw new InsufficientFundsException(balancePence, pence);
balancePence -= pence; // invariant preserved on every path
}
}
Validate at the boundary, fail loudly, and every other method can assume the invariant instead of re-checking it — that assumption is what makes the rest of the class simple.
Exceptions: a hierarchy with intent
try {{ ... }} catch (Exception e) {{}} is not error handling; it is evidence destruction. The design rule: checked exceptions for recoverable conditions the caller should plan for (file missing, network down); unchecked exceptions for programming errors (violated precondition, impossible state) — those should crash noisily in development, because the fix is a code change, not a retry. Design small hierarchies (PaymentException ← CardDeclinedException, GatewayTimeoutException) so callers can catch at the level of specificity they can actually handle.
try { ... } catch (Exception e) {} is not error handling; it is evidence destruction.Immutability: deleting whole bug classes
public record Money(long pence, Currency currency) {
public Money {
if (pence < 0) throw new IllegalArgumentException("negative money");
}
public Money plus(Money other) {
if (!currency.equals(other.currency)) throw new CurrencyMismatchException();
return new Money(pence + other.pence, currency); // new value, no mutation
}
}
An immutable object's invariants are checked once, in the constructor, and can never be violated afterwards — no defensive copying, no state corruption, and thread-safety for free (the deep reason, shared mutability, is explored in Concurrency). Modern Java's record makes the pattern one line; use it as your default and reach for mutable classes only with a reason — advice matching Bloch's classic guidance to minimise mutability and, further down this page, to favour composition over inheritance by default2.
3. Structural Decomposition: Beyond God Classes
The beginner failure mode is one massive class holding UI, file I/O and business logic — the God class. Two forces keep structure healthy:
- High cohesion: a class does exactly one thing well; every field is used by most methods. If you can't name the class without "And" or "Manager", it is probably two classes.
- Low coupling: classes know as little about each other as possible — depend on interfaces, communicate through narrow contracts, never reach through an object into its internals (
order.getCustomer().getAddress().getCity()is a coupling chain, and every link is a reason to break later).
Composition over inheritance is the operational rule: reach for inheritance only for a true, permanent is-a relationship with behavioural substitutability (see the Liskov discussions in SOLID), and for everything else compose — a has-a wired at construction time. Deep inheritance trees are rigid because every subclass inherits every ancestor's decisions forever; composed objects can be rewired per use. The full argument, with the classic Square/Rectangle failure, lives in Inheritance & Composition.
4. Generics and Contracts: The Type System as Ally
Generics: polymorphism checked at compile time
List<String> names = new ArrayList<>(); // interface on the left, type argument checked
names.add("Ada");
String first = names.get(0); // no cast, no ClassCastException at 2am
Raw types (List without a parameter) push type errors from compile time to run time — the compiler is offering to prove a theorem about your program, and raw types decline the offer. The declaration also shows the interface-first habit: List<String> on the left, implementation on the right, so the implementation can change without touching any caller (expanded in Data Structures as Behavioural Contracts).erasure means the check ends at compile time
Abstract classes vs. interfaces
The decision rule: an interface defines a pure behavioural contract — what you promise, nothing about how (and a class can promise many). An abstract class provides a structural skeleton — shared state and partial implementation for a family of closely-related types (template method pattern in Polymorphism). Since default methods, the practical gap narrowed; the remaining question is simply: is there shared state? No → interface. Yes → abstract class — and check the inheritance is truly is-a.
For all of these ideas pushed to an extreme — interfaces, records, promotion, laziness — see the Numerical Tower case study.
Related Topics
- Data Structures as Behavioural Contracts — the collections framework whose Big-O trade-offs this page's JVM/memory model explains from underneath.
- Case Study: The Numerical Tower — a worked Java example applying this page's invariant-protection and immutability discipline to build a hierarchy of safely-promoting number types.
- SOLID Principles — the Single Responsibility Principle behind this page's warning against God classes accumulating unrelated responsibilities.
- Trustworthy Software — the verifiability pillar this page's defensive-programming and invariant-protection advice is building toward.
References
Oracle / Gosling, J., Joy, B., Steele, G., Bracha, G., Buckley, A., Smith, D., & Bierman, G. The Java Language Specification, §8.4.1 and Chapter 15 (method invocation and parameter passing). https://docs.oracle.com/javase/specs/ ↩
Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Item 17 ("Minimize mutability") and Item 18 ("Favor composition over inheritance"). ↩