🪆 Enums, Records & Nested Types · Intermediate

Inner classes in Java

Tied to an outer instance; Outer.this; memory-leak risk.

🧩 The mysteryA screen closes, yet its 50 MB of images never leave memory. The culprit is a tiny class that holds an invisible reference. Where is it hiding?

Glued to an outer object

An inner class is a nested class without static. Every inner object belongs to an outer instance and keeps a hidden reference to it. That's why inner code can use the outer's fields directly.

class Outer {
    int x = 1;
    class Inner {
        int get() { return x; }  // Outer's x
    }
}

Creating one & Outer.this

From outside you need an outer object first: **outer.new Inner()**. When names clash, plain x means the inner's own field, and **Outer.this.x** reaches the enclosing object's field.

Outer o = new Outer();
Outer.Inner in = o.new Inner();
// or: new Outer().new Inner()
🔮 Predict it

Shadowed names

Both classes have a field n. What prints?

class Box {
    int n = 3;
    class Lid {
        int n = 5;
        int f() { return n * 100 + Box.this.n; }
    }
}
void main() {
    System.out.println(new Box().new Lid().f());
}
  1. 503
  2. 305
  3. 505
  4. 303
Show the answer

Plain n is Lid's own field (5) — it shadows Box's. Box.this.n is the enclosing object's field (3). 5 * 100 + 3 = 503.

⚠️ The trap

No outer, no inner

Writing new Outer.Inner() with no outer instance is a compile error — the inner object would have nobody to point to. Write new Outer().new Inner(), or make the class static if it doesn't need the outer object.

class Outer { class Inner { } }
var i = new Outer.Inner();         // error
var j = new Outer().new Inner();   // ok
🤔 Think first

The leaking screen

A screen holding 50 MB of images registers an *inner-class* listener with an app-wide event bus that lives forever. The screen closes, but memory never drops. Why — and what's the cleanest fix?

Think about it, then reveal the answer

The bus keeps the listener alive, and the listener's hidden Outer.this keeps the whole screen reachable, images included. Fix: make the listener a static nested class that receives only the data it needs (and unregister it when the screen closes).

💼 In the real world

A famous warning

Android developers met this leak so often that Android's lint tool warns "This Handler class should be static or leaks might occur". The same bug appears in servers with long-lived caches and schedulers that hold inner-class tasks.

Key takeaways

  1. outer.new Inner() — needs an outer instance
  2. Outer.this.x reaches the outer field when names clash
  3. Every inner object holds a hidden reference to its outer object
  4. Leak risk: a long-lived inner object keeps its outer alive

💡 A balloon tied to a child's wrist: wherever the balloon goes, the child is still attached.

🤯 Did you know?

Inner classes arrived in Java 1.1 (1997) without changing the JVM: javac quietly adds a hidden field, named this$0, that stores the outer object.

Practice questions

What does this print?

class Outer {
    int x = 1;
    class Inner {
        int x = 2;
        int f() { return x * 10 + Outer.this.x; }
    }
}
void main() {
    System.out.println(new Outer().new Inner().f());
}
  1. 21
  2. 22
  3. 12
  4. 11
Check your answer

21. Plain x is Inner's own field (2); Outer.this.x is the enclosing object's field (1). 2 * 10 + 1 = 21.

What does this print?

class Outer {
    class Inner { }
}
void main() {
    var i = new Outer.Inner();
    System.out.println("made");
}
  1. made
  2. Compile error
  3. Throws NullPointerException
Check your answer

Compile error. Inner needs an enclosing Outer object, and none is given. new Outer().new Inner() would work.

Next: you can declare a class *inside a method* — and it can peek at that method's local variables.