Why this matters
- Non-static inner classes hold a hidden reference to their enclosing instance, which is a real and common cause of memory leaks.
- Immutability removes the need for synchronisation entirely, which is why it is the foundation of safe concurrent code.
- Records, covered in the Java 17 module, are the language's answer to the immutable-class boilerplate shown here — but the manual recipe is still what you need for anything with more than plain data.
Four kinds of nested class
- Static nested class — declared
static. It has no link to an enclosing instance and behaves like a top-level class that happens to live inside another for namespacing.Map.Entryis the familiar example. - Inner (non-static) class — holds an implicit reference to the enclosing object, so it can read its private fields. It cannot exist without an instance of the outer class.
- Local class — declared inside a method body. Visible only there.
- Anonymous class — declared and instantiated in one expression, with no name. Largely replaced by lambdas for single-method interfaces.
public class Outer {
private String secret = "outer state";
static class StaticNested {
void show() {
// no access to `secret` — there is no enclosing instance
}
}
class Inner {
void show() {
System.out.println(secret); // reaches private outer state
}
}
void method() {
class Local {
void show() { System.out.println(secret); }
}
new Local().show();
Runnable anonymous = new Runnable() {
@Override public void run() { System.out.println(secret); }
};
anonymous.run();
}
}
Creating them differs, and the inner-class syntax surprises people:
Outer.StaticNested nested = new Outer.StaticNested(); // no outer instance needed
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // requires one
Anonymous classes versus lambdas
// Anonymous class: verbose, but works for any interface or abstract class
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// Lambda: only for functional interfaces, far shorter
Comparator<String> shorter = (a, b) -> Integer.compare(a.length(), b.length());
| Aspect | Anonymous class | Lambda |
|---|---|---|
| Works with | Any interface or abstract class | Functional interfaces only |
| Can hold state | Yes — fields and several methods | No |
| Meaning of this | The anonymous instance itself | The enclosing instance |
| Compiled to | A separate class file | An invokedynamic call site |
| Use when | You need multiple methods or state | Almost always otherwise |
Works with
Anonymous classAny interface or abstract classLambdaFunctional interfaces onlyCan hold state
Anonymous classYes — fields and several methodsLambdaNoMeaning of this
Anonymous classThe anonymous instance itselfLambdaThe enclosing instanceCompiled to
Anonymous classA separate class fileLambdaAn invokedynamic call siteUse when
Anonymous classYou need multiple methods or stateLambdaAlmost always otherwise
The different meaning of `this` is the detail that catches people migrating from one to the other.
Both capture local variables only if those variables are effectively final — assigned once and never changed. The compiler copies the value, so allowing reassignment would make the copy and the original disagree.
Immutable objects
An object is immutable when nothing can change its observable state after construction. Five steps make that true.
The recipe
- Declare the class
finalso no subclass can add mutable state or override a method to lie. - Make every field
private final. - Provide no setters and no method that modifies a field.
- Copy mutable constructor arguments so the caller's reference cannot reach your state.
- Never return a mutable field directly — return a copy or an unmodifiable view.
public final class Itinerary {
private final String traveller;
private final LocalDate departure; // immutable type: safe as-is
private final List<String> stops; // mutable type: needs care
public Itinerary(String traveller, LocalDate departure, List<String> stops) {
this.traveller = Objects.requireNonNull(traveller);
this.departure = Objects.requireNonNull(departure);
this.stops = List.copyOf(stops); // defensive copy, already unmodifiable
}
public String traveller() { return traveller; }
public LocalDate departure() { return departure; }
public List<String> stops() { return stops; } // safe: the copy is unmodifiable
/** Changes produce a new object rather than mutating this one. */
public Itinerary withDeparture(LocalDate newDate) {
return new Itinerary(traveller, newDate, stops);
}
}
The withX method is the idiomatic way to offer "modification" on an immutable type. It returns a
new instance and leaves the original untouched, exactly as String.toUpperCase() does.
What immutability buys you
- Thread safety for free. No state changes, so there is nothing to synchronise and no race to lose.
- Safe as a map key. A mutable key whose
hashCodechanges after insertion becomes unfindable; an immutable one cannot. - Safe to share and cache. No defensive copying at every boundary, because no caller can do damage.
- Simpler reasoning. A value that cannot change needs no timeline. You read the constructor and you know everything.
- Reliable
equalsandhashCode. Computed once from fixed fields, and cacheable.
The cost is allocation: every change creates an object. For small objects this is cheap, and generational garbage collection is specifically optimised for short-lived ones. Where it genuinely matters, as in string building inside a loop, use the mutable builder and produce an immutable result at the end.
Common misreadings
- "
finalon a field makes the object immutable." It fixes the reference, not the object.private final List<String> itemscan still be cleared. - "Inner and nested classes are the same thing." "Inner" specifically means non-static, with an enclosing instance. The difference is the leak risk.
- "A lambda is just shorthand for an anonymous class."
thismeans different things, and they compile differently. The resemblance is superficial. - "Immutable classes cannot have collections." They can, with a defensive copy in and an unmodifiable view out.
- "Immutability is slow." Extra allocation of short-lived objects is what generational collectors handle best. Measure before assuming.
Quick recall
Everything you need if you only revisit this box.
- Four nested forms: static nested (no outer link), inner (implicit outer reference), local, anonymous.
- Default to
staticnested. A non-static inner class keeps its enclosing object alive — a real leak source. - Captured locals must be effectively final, because the value is copied.
- In an anonymous class
thisis the anonymous instance; in a lambda it is the enclosing instance. - Immutability recipe: final class, private final fields, no setters, copy in, copy or wrap out.
- Offer change through
withXmethods returning a new instance. List.copyOfsnapshots;Collections.unmodifiableListonly wraps the caller's list.- Immutable objects are thread-safe, safe as map keys, and safe to cache and share without copying.
Test yourself
Answer these before moving on — recall is what makes it stick.
- Differentiate between static nested class, inner class, local class, and anonymous class.
- After Java 8 default methods, when should you prefer an abstract class over an interface?
- What are the commonly overridden methods from Object class (equals, hashCode, toString, clone)? How do you override them correctly?