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. Method calls copy the reference, so callee and caller see the same object — but reassigning the parameter inside the method changes nothing outside:
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.
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:
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.
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.
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).
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.