skip to content
everything.oop.nestedClasses

nested and inner classes

static nested, inner, local, anonymous. and the hidden this$0.

chapter
chapter 03.11
reading time
6 min read

a class declared inside another comes in four kinds: static nested (just a class with a longer name), inner (every instance holds a hidden reference, this$0, to the outer object that made it), local and anonymous (declared inside a method, and they also copy the local variables they use into hidden fields). that hidden outer reference is the classic java memory leak.

the picture

think of a backpack (the outer object) and a keychain clipped onto it (an inner object). you can carry the keychain around on its own, but it's still clipped to the backpack, so wherever the keychain goes, the backpack comes too. a static nested class is a keychain sold separately: same brand, no clip.

kind =
class Outer {
    String name;
    class Inner {
        String hi() { return name; }
    }
}

var i = outer.new Inner();
javac writes
Outer$Inner.class
hidden fields it adds
final Outer this$0
stack
main
outer
i
heap
Outer$Inner
this$0
Outer
name
big
String
"outer"
byte[]
10,000,000 bytes

an inner class reads Outer's fields, so every Inner carries a hidden reference to the Outer that made it. you can't even create one without an Outer: outer.new Inner().

pick a kind. the right side is what javac actually generates (checked with javap -p). the memory pane shows one instance: watch for the this$0 arrow back to the Outer, and its 10 MB big array riding along.
kinddeclaredneeds an outer object?can see
static nestedstatic class X in a classnoouter's static members, and its privates
innerclass X in a classyes: outer.new X()everything in the outer object
localclass X inside a methodif the method isn't staticthe outer object, and effectively final locals
anonymousnew Iface() {"{ ... }"}if the method isn't staticsame as local

proof: the hidden fields are real

Main.java
import java.lang.reflect.Field;
import java.util.function.Supplier;
 
public class Main {
    private String name = "outer";
 
    class Inner {
        String hi() { return "inner of " + name; }
    }
 
    static class Nested {
        String hi() { return "nested"; }
    }
 
    Supplier<String> anon(String greeting) {
        return new Supplier<>() {
            public String get() { return greeting + ", " + name; }
        };
    }
 
    static void fields(Object o) {
        StringBuilder sb = new StringBuilder(o.getClass().getName() + ":");
        for (Field f : o.getClass().getDeclaredFields()) sb.append(" ").append(f.getName());
        System.out.println(sb);
    }
 
    public static void main(String[] args) {
        Main outer = new Main();
        Main.Inner inner = outer.new Inner();
        Main.Nested nested = new Main.Nested();
        Supplier<String> s = outer.anon("hi");
        fields(inner);
        fields(nested);
        fields(s);
        System.out.println(inner.hi() + " / " + s.get());
    }
}
output, from jdk 25
Main$Inner: this$0
Main$Nested:
Main$1: val$greeting this$0
inner of outer / hi, outer

this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.

you never wrote a field called this$0 or val$greeting. javac did. the inner class reads name, so it needs the Main. the anonymous class reads greeting, which lives in a stack frame that will be gone by the time get() runs, so javac copies it into the object.

one level deeper:turns out javac skips this$0 when it can (and constants fool you)+

since jdk 18 (JDK-8271623), javac only emits this$0 if the inner class actually uses the outer instance. an inner class that never touches Outer still takes an Outer constructor parameter, but throws it away, so it can't leak it. (serializable inner classes without a serialVersionUID still get the field, for compatibility.)

and a sneaky one: if name above were private final String name = "outer";, it'd be a compile-time constant, javac would fold "inner of " + name into "inner of outer", and Inner would have no this$0 at all. same source, different class file. fun fact for compact source files: their top-level classes become inner classes of the implicit class, so class Dog in a void main() file compiles to Main$Dog, whose constructor takes a hidden Main parameter.

the leak

an inner, local or anonymous object that outlives its outer object keeps the outer object alive. register one as a listener in something long-lived, and the whole outer object (and everything it points to) can never be collected.

Main.java next to run
  1. 1import java.util.*;
  2. 2
  3. 3public class Main {
  4. 4 static final List<Runnable> listeners = new ArrayList<>();
  5. 5
  6. 6 static class Screen {
  7. 7 final byte[] pixels = new byte[10_000_000];
  8. 8
  9. 9 void open() {
  10. 10 listeners.add(new Runnable() {
  11. 11 public void run() { redraw(); }
  12. 12 });
  13. 13 }
  14. 14
  15. 15 void redraw() { }
  16. 16 }
  17. 17
  18. 18 public static void main(String[] args) {
  19. 19 Screen s = new Screen();
  20. 20 s.open();
  21. 21 s = null;
  22. 22 System.out.println(listeners.size());
  23. 23 }
  24. 24}
stdout
·
stack
main
no locals yet
heap
class Mainstatic fields
listeners
ArrayList<Runnable>
empty

a long-lived static list of listeners. it lives as long as the class does, which is basically forever.

step 1/6
step 5 is the leak: s = null, and the Screen is still reachable through the static list, the listener, and its hidden this$0. no ghost appears because nothing became unreachable. that's the bug.

the fixes, in order of preference:

  • make it static (a static nested class, or a top-level one) and pass in only what it needs. no this$0 possible.
  • use a lambda that doesn't touch this. a lambda only captures what it uses; if it never mentions an instance member, it doesn't capture the outer object. see lambdas.
  • unregister listeners when the owner is done (close(), dispose()), or hold them through a WeakReference.

more ways to leak, and how to find them in a heap dump: memory leaks.

capturing locals: effectively final

local and anonymous classes (and lambdas) copy captured locals into fields when they're created. if java let you change the local afterwards, the copy and the original would silently disagree. so the rule is: you can only capture a local that's effectively final (never assigned after initialization).

Main.java
void main() {
    int count = 0;
    Runnable r = new Runnable() {
        public void run() {
            IO.println(count);
        }
    };
    count++;
}

error: local variables referenced from an inner class must be final or effectively final. the lambda version says the same thing with "a lambda expression" in it. the usual escape hatch is a one-element array or an AtomicInteger, which keeps the reference final while the contents change. fields are different: an inner class reads them live through this$0, so they can change freely.

quick check
which one can be created without an instance of Outer?
keylines
  • static nested: a normal class with a longer name. no outer object.
  • inner: every instance holds this$0, a hidden reference to its outer object.
  • local and anonymous: this$0 plus copies of captured locals (val$x).
  • captured locals must be effectively final, because the class holds a copy.
  • an inner object that outlives its outer keeps it alive. default to static.