PrepZone Logo
PrepZone

Nested Classes and Immutable Objects

Four kinds of nested class, and the recipe for an object nobody can modify after construction.

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 nestedNo outer instance needed
inner (non-static)Holds a hidden outer reference
localDeclared inside a method
anonymousOne-off implementation
The only real question is whether the nested type needs a reference to the enclosing instance. If it does not, make it static.
  • 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.Entry is 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.
Java
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:

Java
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

Java
// 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());
AspectAnonymous classLambda
Works withAny interface or abstract classFunctional interfaces only
Can hold stateYes — fields and several methodsNo
Meaning of thisThe anonymous instance itselfThe enclosing instance
Compiled toA separate class fileAn invokedynamic call site
Use whenYou need multiple methods or stateAlmost always otherwise
  • Works with

    Anonymous classAny interface or abstract class
    LambdaFunctional interfaces only
  • Can hold state

    Anonymous classYes — fields and several methods
    LambdaNo
  • Meaning of this

    Anonymous classThe anonymous instance itself
    LambdaThe enclosing instance
  • Compiled to

    Anonymous classA separate class file
    LambdaAn invokedynamic call site
  • Use when

    Anonymous classYou need multiple methods or state
    LambdaAlmost 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.

final classNobody can subclass and add setters
private final fieldsAssigned once, in the constructor
Defensive copy innew ArrayList<>(input)
Defensive copy outList.copyOf(items)
Final fields are not enough. A mutable field such as a list must be copied on the way in and on the way out, or callers can still reach inside.

The recipe

  • Declare the class final so 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.
Java
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 hashCode changes 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 equals and hashCode. 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

  • "final on a field makes the object immutable." It fixes the reference, not the object. private final List<String> items can 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." this means 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 static nested. 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 this is 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 withX methods returning a new instance.
  • List.copyOf snapshots; Collections.unmodifiableList only 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.