Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 5 – Classes, objects, records and enums

Java nested, inner, local and anonymous classes

Choose between static nested, inner, local and anonymous Java classes, see the hidden reference an inner class keeps, and swap anonymous classes for lambdas.

  • Intermediate
  • 20 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Choose between static nested, inner, local and anonymous classes for a helper
  • Explain the hidden reference an inner class keeps to its outer object, and the leak it can cause
  • Predict which local variables a local class, an anonymous class or a lambda may capture
  • Replace anonymous classes with lambdas where the interface has one method, and say what this means in each
  • Predict which classes of a compact source file are inner classes

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between static nested, inner, local and anonymous classes for a helper
  • Explain the hidden reference an inner class keeps to its outer object, and the leak it can cause
  • Predict which local variables a local class, an anonymous class or a lambda may capture
  • Replace anonymous classes with lambdas where the interface has one method, and say what this means in each
  • Predict which classes of a compact source file are inner classes

Before you start

On this page

Compile a playlist class and the program that plays it, two source files, and javac writes six class files. Four of them hold classes declared inside other code: a nested class: A class declared inside another class or inside a method. It is static nested, inner, local or anonymous, and it may use the private members of the classes around it. is declared inside another class or a method, and Java has four kinds: static nested class: A class declared with static inside another class. It has no enclosing object, so new Outer.Nested() needs none, and it reaches the outer class's instance members only through a reference it is given., inner class: A nested class declared without static. Each of its objects belongs to one object of the outer class, its enclosing instance, which it can name as Outer.this; local and anonymous classes are inner classes too., local class: A class declared inside a method or another block. It is known only there, and it may read the block's local variables and parameters that are final or effectively final. and anonymous class: A class without a name, declared and instantiated in one expression, as in new Comparator<String>() { … }. It implements an interface or extends a class, and javac names its class file Outer$1, Outer$2 and so on..

Nesting keeps a helper next to the only code that uses it, and lets it read the outer class’s private members. The kinds differ in one important way: inner, local and anonymous classes can see an enclosing object or local variables, while a static nested class cannot. That difference decides memory use, what the class may capture and what this means inside it. This lesson puts all four kinds into one class, shows with reflection the hidden reference an inner class keeps, rewrites an anonymous class as a A short way to write an object of an interface that has one abstract method: the parameters, an arrow and the body, as in (x, y) -> Integer.compare(x, y). The module on lambdas covers them in depth., and shows why every class in a compact source file is an inner class.

Java has four places to declare a class inside another

A static nested class is declared with static inside a class and is an ordinary class that happens to live in another’s namespace. An inner class is declared inside a class without static, and each of its objects belongs to one object of the outer class. A local class is declared inside a method or another block, and an anonymous class is declared and created in one expression, without a name. Playlist holds one nested class of each of the first three kinds, and Main adds an anonymous comparator:

A playlist with a class of each kind

Java (Playlist.java) · kinds/Playlist.java

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/** A playlist with one class of each kind inside it. */
final class Playlist implements Iterable<String> {
    private final List<String> songs;

    private Playlist(List<String> songs) {
        this.songs = List.copyOf(songs);
    }

    /** Static nested: needs no Playlist object, and builds one step by step. */
    static final class Builder {
        private final List<String> songs = new ArrayList<>();

        Builder add(String song) {
            songs.add(song);
            return this;
        }

        Playlist build() {
            return new Playlist(songs);
        }
    }

    /** Inner: every Cursor belongs to one Playlist object and reads that playlist's songs. */
    private final class Cursor implements Iterator<String> {
        private int next = 0;

        @Override
        public boolean hasNext() {
            return next < songs.size();
        }

        @Override
        public String next() {
            if (!hasNext()) throw new NoSuchElementException();
            return songs.get(next++);
        }
    }

    @Override
    public Iterator<String> iterator() {
        return new Cursor();              // the new Cursor is tied to this playlist
    }

    /** Local: declared inside the method, and it may read the method's effectively final variables. */
    List<String> longerThan(int letters) {
        class Filter {
            boolean keep(String song) {
                return song.length() > letters;
            }
        }
        Filter filter = new Filter();
        List<String> kept = new ArrayList<>();
        for (String song : songs) {
            if (filter.keep(song)) kept.add(song);
        }
        return kept;
    }
}

Java (Main.java) · kinds/Main.java

void main() {
    Playlist playlist = new Playlist.Builder().add("Monsoon Song").add("Rain").add("Night Train Home").build();
    for (String song : playlist) IO.println("plays " + song);             // the for-each loop uses a Cursor
    IO.println("longer than 10 characters: " + playlist.longerThan(10));

    // Anonymous: a class without a name, declared and instantiated in one expression.
    Comparator<String> byLength = new Comparator<String>() {
        @Override
        public int compare(String a, String b) {
            return Integer.compare(a.length(), b.length());
        }
    };
    List<String> sorted = new ArrayList<>();
    playlist.forEach(sorted::add);
    sorted.sort(byLength);
    IO.println("shortest first: " + sorted);
}
The program, then the class files javac writes Python · kinds_session.py
from session import run

# The program, then the class files javac writes for it: one per class, nested ones included.
run("""
java kinds/Main.java
javac -d classes kinds/Main.java kinds/Playlist.java
ls classes
""")

Output

# openjdk version "25.0.4.1" LTS
$ java kinds/Main.java
plays Monsoon Song
plays Rain
plays Night Train Home
longer than 10 characters: [Monsoon Song, Night Train Home]
shortest first: [Rain, Monsoon Song, Night Train Home]
$ javac -d classes kinds/Main.java kinds/Playlist.java
$ ls classes
Main.class
Main$1.class
Playlist.class
Playlist$1Filter.class
Playlist$Builder.class
Playlist$Cursor.class

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kinds_session.py

  • Builder is static nested. new Playlist.Builder() needs no playlist, which is the point: it exists to create one. Being nested, it may call the private constructor of Playlist. Builders and small helper types are the typical static nested classes.
  • Cursor is inner. iterator() returns new Cursor(), and that cursor reads the songs field of the very playlist that created it. The for loop over a playlist uses one, without ever naming the class.
  • Filter is local. It exists only inside longerThan, and it reads the method’s parameter letters.
  • The comparator in Main is anonymous. new Comparator<String>() { … } declares a class that implements the interface and creates its only object in the same expression.

The listing of classes shows that each one is a real class with its own class file. Member classes are named Outer$Member, local classes Outer$1Filter and anonymous classes Outer$1, Outer$2 and so on. Those names appear in stack traces, so it pays to recognise them.

Checkpoint

Check: The four kinds of nested class

  1. Question 1 of 2 A Cache class needs a small Entry class that holds a key, a value and an expiry time, and Entry never uses the Cache it lives in. Which kind should Entry be?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A static nested class, because it needs no Cache object

    A nested class that does not need the outer object should be static. An inner Entry would keep its Cache reachable for no reason, and its constructor would need a Cache.

  2. Question 2 of 2 javac compiled Playlist.java. What is the class file of the local class Filter, declared inside longerThan, called?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Playlist$1Filter.class

    Member classes are named Outer$Member, local classes Outer$1Name and anonymous classes Outer$1, Outer$2 and so on. Those names appear in stack traces.

An inner class gets a hidden field for its outer object

An inner object belongs to an outer object, its enclosing instance: The object of the outer class that an inner object belongs to. The compiler passes it to the inner class's constructor and, when the inner class uses it, keeps it in a hidden field named this$0., so the compiler has to connect them. It passes the outer object to the inner class’s constructor and, when the inner class actually uses it, stores it in a Made by the compiler rather than written in the source code, such as the field that holds an inner object's outer object. field named this$0. Reflection shows the difference between three nested classes of a Screen:

hidden/Screen.java Java · hidden/Screen.java
/** Three nested classes of Screen, to see which ones keep a reference to a Screen object. */
final class Screen {
    private final String title;

    Screen(String title) {
        this.title = title;
    }

    /** Inner, and it reads the field of its Screen. */
    final class TitleLabel {
        String text() {
            return "[" + title + "]";
        }
    }

    /** Inner, but it never touches its Screen. */
    final class Spacer {
        String text() {
            return "     ";
        }
    }

    /** Static nested: no Screen involved at all. */
    static final class Style {
        String text() {
            return "bold";
        }
    }
}
Fields and constructors the compiler generated Java · hidden/Main.java
// What reflection shows about the fields and constructors the compiler gave each nested class.
void main() throws Exception {
    for (Class<?> c : List.of(Screen.TitleLabel.class, Screen.Spacer.class, Screen.Style.class)) {
        IO.println(c.getName());
        IO.println("  fields:      " + Arrays.toString(c.getDeclaredFields()));
        IO.println("  constructor: " + Arrays.toString(c.getDeclaredConstructors()));
    }

    Screen home = new Screen("Home");
    Screen.TitleLabel label = home.new TitleLabel();     // an inner object needs an outer one
    Field outer = Screen.TitleLabel.class.getDeclaredField("this$0");
    outer.setAccessible(true);
    IO.println("label's hidden field holds home: " + (outer.get(label) == home));
    IO.println("label.text() = " + label.text());
}

Output

Screen$TitleLabel
  fields:      [final Screen Screen$TitleLabel.this$0]
  constructor: [Screen$TitleLabel(Screen)]
Screen$Spacer
  fields:      []
  constructor: [Screen$Spacer(Screen)]
Screen$Style
  fields:      []
  constructor: [Screen$Style()]
label's hidden field holds home: true
label.text() = [Home]

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

  • TitleLabel reads title, so it has the field final Screen this$0, and the run confirms that it holds the very screen the label was created from. home.new TitleLabel() is how code outside the class names that screen.
  • Spacer never touches its screen, and it has no field at all. The JDK 18 release notes describe the change: before JDK 18, javac always generated the field; since then it omits it when the inner class does not use its enclosing instance. The constructor still takes a Screen, so new Spacer() still needs one.
  • Style is static nested: no field, and a constructor without a Screen parameter.

Step through the second half of the program, where the label is created and asked for its text:

Interactive · message by message What home.new TitleLabel() passes and keeps Step through the second half of hidden/Main.java and watch where the Screen goes.
What home.new TitleLabel() passes and keeps, step 6 of 6. Messages so far: main to home: new Screen("Home"); main to label: home.new TitleLabel(); home to label: passed to the constructor; label to home: read title via this$0; label to main: "[Home]".mainhomeScreen("Home")labelthis$0 = homenew Screen("Home")home.new TitleLabel()passed to the constructorread title via this$0"[Home]"

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. An outer object first. main creates the screen with new Screen("Home"). An inner object always needs one, so this comes first.
  2. home.new TitleLabel(). home.new TitleLabel() creates the inner object and names its outer object, home. Inside an instance method of Screen, plain new TitleLabel() would pass this.
  3. The hidden parameter. The compiler gave the constructor a parameter for the outer object, Screen$TitleLabel(Screen), as reflection lists. home arrives through it.
  4. The hidden field. Because TitleLabel reads title, the constructor stores home in the synthetic field this$0. The run confirms it holds home: true.
  5. text() reads through it. label.text() reads title through this$0, which is how an inner object reads its outer object's private field.
  6. The result. text() returns [Home], the title of the very screen the label was created from.

Checkpoint

Check: The hidden field of an inner class

  1. Question 1 of 2 On JDK 25, an inner class reads a field of its outer class. What does the compiler generate to make that work?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A constructor parameter for the outer object, kept in a hidden field named this$0

    The outer object is passed to the inner class's constructor and kept in the synthetic field this$0 when the inner class uses it.

  2. Question 2 of 2 Spacer is an inner class that never uses its Screen. On JDK 25, what does reflection show for it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No field, and a constructor that still takes a Screen

    The field depends on use, the constructor parameter does not: new Spacer() still needs a Screen to pass.

The hidden field keeps the outer object alive

The reference matters for memory. As long as anything can reach a TitleLabel, its screen cannot be garbage collected, together with everything the screen holds:

A static list holds an inner TitleLabel, whose hidden this$0 field keeps its Screen reachable; Spacer and Style hold nothing.Kept aliveHold no Screenstatic listof listenersTitleLabelinner, reads titleits Screen andeverything it holdsSpacerinner, never uses it:no field (JDK 18+)Stylestatic nestedholdsthis$0

An inner object that uses its outer object keeps it alive

Text description of the diagram

The diagram has two groups, one above the other: what is kept alive, and what holds no Screen.

  1. A long-lived, static list of listeners holds a TitleLabel object.
  2. TitleLabel is an inner class that reads the title of its Screen, so the compiler gave it a hidden field, this$0, that refers to the Screen object. As long as the list holds the label, the Screen and everything the Screen holds cannot be garbage collected.
  3. Spacer is also an inner class, but it never uses its Screen. Since JDK 18 the compiler omits the hidden field in that case, so a Spacer object refers to nothing.
  4. Style is a static nested class. Its objects never have an enclosing Screen at all.

That is the classic leak of inner classes: an inner object registered as a listener in some long-lived list, or kept in a cache, keeps its whole outer object alive long after the outer object was meant to go. Step through it as it happens:

Interactive · step by step A closed screen that the garbage collector cannot reclaim Step through what stays reachable after a screen is closed, starting from the static list.
A closed screen that the garbage collector cannot reclaim, step 5 of 5. Showing static list, TitleLabel, its Screen (closed), the app (no longer refers to it), Style, Spacer. In focus: Style, Spacer.holdsthis$0static listlives as long as the appTitleLabelinner, reads titleits Screenclosedthe appno longer refers to itStylestatic nestedSpacerinner, never uses it

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The screen is open. The app refers to the screen, and the screen is in use.
  2. A listener is registered. The screen's TitleLabel goes into a static list of listeners. TitleLabel reads title, so its hidden field this$0 refers to the screen.
  3. The screen is closed. The app drops its reference. Nothing that the app holds leads to the screen any more.
  4. Still reachable. The garbage collector follows references from the static list: list, then TitleLabel, then this$0, then the screen and everything the screen holds. Reachable objects are kept, so the closed screen stays in memory.
  5. Nested classes that hold nothing. A Spacer never uses its screen, so since JDK 18 it has no this$0 field, and a static nested Style never has an outer object. Neither of them could keep a screen alive.

The release note makes the same point: omitting unused this$0 fields lets outer objects be collected sooner. The cure is the first rule of choosing a kind: make a nested class static unless it really needs the outer object. A static nested class cannot keep one by accident.

Serialization is the other reason to avoid inner classes where you can. The Learn Java guide on nested classes says that serializing inner classes, including local and anonymous ones, is strongly discouraged, because the synthetic constructs the compiler adds for them can differ between compilers.

Key idea

An inner object that uses its outer object holds it through this$0, so whatever keeps the inner object keeps the outer one too: make a nested class static unless it needs the outer object.

Checkpoint

Check: Inner objects that keep their outer object alive

  1. Question 1 of 2 A closed screen's TitleLabel is still in a static list of listeners. Why can the garbage collector not reclaim the screen?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The list reaches the label, and the label's this$0 field reaches the screen

    Whatever holds an inner object also holds its outer object, and everything that object holds.

  2. Question 2 of 2 Which object of hidden/Screen.java, kept in a static list, would keep its Screen in memory?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A TitleLabel

    Only an inner object that uses its outer object carries a reference to it, which is why static nested classes, and inner classes that leave the outer object alone, cannot leak a screen.

Local and anonymous classes capture only effectively final variables

A local or anonymous class, and a lambda, may read the local variables and parameters of the method around it, but only if they are final or effectively final: Said of a local variable or parameter that is never changed after it is assigned, so adding final to it would not cause a compile error. Local and anonymous classes and lambdas may read only such variables or final ones.: assigned once and never changed. The specification lists the exact conditions (never assigned again, never the operand of ++ or --) and adds a quick test: adding final to an effectively final variable causes no compile error.

Changing a captured variable

Java (LocalClass.java) · capture/LocalClass.java

void main() {
    int limit = 3;
    class Check {
        boolean ok(int n) {
            return n <= limit;            // reads a local variable of main
        }
    }
    limit = 4;                            // so this assignment is an error
    IO.println(new Check().ok(4));
}

Java (Lambda.java) · capture/Lambda.java

void main() {
    int count = 0;
    Runnable report = () -> IO.println("count is " + count);
    count++;                              // the same rule for a lambda
    report.run();
}
javac refuses both Python · capture_session.py
from session import run

run("""
!java capture/LocalClass.java
!java capture/Lambda.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java capture/LocalClass.java
capture/LocalClass.java:5: error: local variables referenced from an inner class must be final or effectively final
            return n <= limit;            // reads a local variable of main
                        ^
1 error
error: compilation failed
[exit status 1]
$ java capture/Lambda.java
capture/Lambda.java:3: error: local variables referenced from a lambda expression must be final or effectively final
    Runnable report = () -> IO.println("count is " + count);
                                                     ^
1 error
error: compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_session.py

The local class reads limit, and the later limit = 4 makes it a variable that changes, which the compiler refuses. Its message calls the local class an inner class, which by the specification’s definition every local class is, apart from local records, enums and interfaces. The lambda gets the same error for count++. The specification gives the reason: capturing local variables that keep changing would likely introduce concurrency problems. In the compiled code, the class or lambda keeps a copy of the value taken when it was created, so a variable that changed afterwards would silently disagree with its copy. When you need a value that changes, keep it in a field, an array element or an object such as an AtomicInteger, and capture the reference to that.

Reading a loop counter in a lambda

for (int i = 0; i < n; i++) tasks.add(() -> use(i)); does not compile, because i++ changes i. Copy it first, int index = i;, inside the loop, and let the lambda read index, a new effectively final variable on every pass.

Checkpoint

Check: Capturing effectively final variables

  1. Question 1 of 2 Does this method compile?

    Read the code, then choose one answer.

    List<Runnable> tasks(List<String> names) {
        List<Runnable> out = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            out.add(() -> IO.println(names.get(i)));
        }
        return out;
    }
    Show the answer: Question 1 of 2

    Answer: No, because i changes with every pass of the loop, so it is not effectively final

    Copying the value first, with int index = i; inside the loop, gives the lambda an effectively final variable of its own for each pass.

  2. Question 2 of 2 Why may a lambda, a local class or an anonymous class read only final or effectively final local variables?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: They keep a copy of the value, and capturing variables that keep changing would likely introduce concurrency problems

    When you need a value that changes, keep it in a field, an array element or an object such as an AtomicInteger, and capture the reference to that.

A one-method anonymous class is usually better as a lambda

An anonymous class that implements an interface with a single abstract method, such as Comparator, Runnable or Predicate, can almost always be written as a lambda, and often as a A lambda shortened to the one method it calls, written with two colons: Song::seconds stands for song -> song.seconds().:

Three ways to write one comparator, one result Java · comparator/Main.java
// One sort written three ways: an anonymous class, a lambda, and a comparator built from a method reference.
record Song(String title, int seconds) {}

void main() {
    List<Song> songs = List.of(new Song("Rain", 214), new Song("Monsoon Song", 187), new Song("Night Train Home", 301));

    List<Song> a = new ArrayList<>(songs);
    a.sort(new Comparator<Song>() {
        @Override
        public int compare(Song x, Song y) {
            return Integer.compare(x.seconds(), y.seconds());
        }
    });

    List<Song> b = new ArrayList<>(songs);
    b.sort((x, y) -> Integer.compare(x.seconds(), y.seconds()));

    List<Song> c = new ArrayList<>(songs);
    c.sort(Comparator.comparingInt(Song::seconds));

    IO.println("anonymous class: " + titles(a));
    IO.println("lambda:          " + titles(b));
    IO.println("comparingInt:    " + titles(c));
}

List<String> titles(List<Song> songs) {
    return songs.stream().map(Song::title).toList();
}

Output

anonymous class: [Monsoon Song, Rain, Night Train Home]
lambda:          [Monsoon Song, Rain, Night Train Home]
comparingInt:    [Monsoon Song, Rain, Night Train Home]

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

All three sorts print the same order. The lambda drops the class declaration, the method signature and the types of the parameters, and Comparator.comparingInt(Song::seconds) states what is compared instead of how. That is the form to prefer for comparators.

The rewrite is not purely mechanical in one respect: this means something else in the two forms.

thisref/Screen.java Java · thisref/Screen.java
/** What `this` means inside an anonymous class and inside a lambda. */
final class Screen {
    private final String name;

    Screen(String name) {
        this.name = name;
    }

    Runnable anonymous() {
        return new Runnable() {
            @Override
            public void run() {
                IO.println("anonymous: this is " + this + "; Screen.this is " + Screen.this);
            }

            @Override
            public String toString() {
                return "the anonymous Runnable";
            }
        };
    }

    Runnable lambda() {
        return () -> IO.println("lambda:    this is " + this);
    }

    @Override
    public String toString() {
        return "Screen " + name;
    }
}
Two answers to the same question Java · thisref/Main.java
void main() {
    Screen home = new Screen("home");
    home.anonymous().run();
    home.lambda().run();
}

Output

anonymous: this is the anonymous Runnable; Screen.this is Screen home
lambda:    this is Screen home

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

Written as this inside it The enclosing screen
An anonymous class The anonymous object, which prints the anonymous Runnable Screen.this
A lambda The screen, Screen home, as in the surrounding code this

Inside the anonymous class, this is the anonymous object itself, which is why its own toString prints, and Screen.this names the enclosing screen. Inside the lambda, this is the screen: the specification says that this and names in a lambda body mean the same as in the surrounding code. Keep an anonymous class when you need this to be the object itself, when the object needs fields of its own, when it must implement more than one method, or when it extends a class rather than implementing an interface.

Checkpoint

Check: Anonymous classes, lambdas and this

  1. Question 1 of 2 Inside a lambda written in an instance method of class Screen, what does this refer to?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The Screen object whose method created the lambda

    The rewrite from an anonymous class to a lambda changes what this means, which matters whenever the body uses this or toString.

  2. Question 2 of 2 When should an anonymous class stay an anonymous class instead of becoming a lambda?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When the object needs fields of its own, more than one method, or a class to extend

    Keep the anonymous class when you need this to be the object itself, fields of its own, several methods, or a superclass.

Every class in a compact source file is an inner class

A compact source file, one whose methods and fields are written at the top level of the file instead of inside a class declaration, is compiled into an implicitly declared class, usually named after the file. The specification says that every class declared in such a file is a member of that implicit class. A plain class declared there is therefore not top level but an inner class, while records, enums and interfaces are implicitly static:

A class and a record in a compact source file

Java (Main.java) · compact/Main.java

// In a compact source file, every class you declare is a member of the implicitly declared class Main.
class Tally {
    int count;
}

record Score(String player, int points) {}

void main() {
    for (Class<?> c : List.of(Tally.class, Score.class)) {
        IO.println(c.getName() + ": member of " + c.getEnclosingClass().getName()
                + ", static " + Modifier.isStatic(c.getModifiers()));
    }
    Tally tally = new Tally();            // fine: main is an instance method, so there is a Main object
    tally.count++;
    IO.println("tally " + tally.count + ", " + new Score("Asha", 42));
}

Java (StaticHelper.java) · compact/StaticHelper.java

// The same inner class, created from a static method of the compact file.
class Tally {
    int count;
}

static Tally fresh() {
    return new Tally();                   // no Main object here, so no enclosing instance for Tally
}

void main() {
    IO.println(fresh().count);
}
Main$Tally is an inner class Python · compact_session.py
from session import run

run("""
java compact/Main.java
!java compact/StaticHelper.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java compact/Main.java
Main$Tally: member of Main, static false
Main$Score: member of Main, static true
tally 1, Score[player=Asha, points=42]
$ java compact/StaticHelper.java
compact/StaticHelper.java:7: error: non-static variable this cannot be referenced from a static context
    return new Tally();                   // no Main object here, so no enclosing instance for Tally
           ^
1 error
error: compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compact_session.py

Tally is Main$Tally and not static; Score is Main$Score and static. Inside void main(), which is an instance method, there is a Main object to attach a new Tally to, so everything works. A static method of the file has no such object, and javac refuses new Tally() there with the message non-static variable this cannot be referenced from a static context. The fix is either to declare static class Tally, or to move the class into its own file, as the examples of this module do once a program grows.

Checkpoint

Check: Classes in a compact source file

  1. Question 1 of 2 A compact source file Main.java declares class Tally { int count; } and a static helper method that calls new Tally(). Why does javac refuse it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Tally is an inner class of the implicit class Main, and a static method has no Main object to attach it to

    Every class declared in a compact source file is a member of the implicitly declared class, so a plain class there needs an enclosing instance. Declaring static class Tally fixes it.

  2. Question 2 of 2 compact/Main.java prints what reflection says about Tally and Score. Which is right?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Tally is Main$Tally and not static; Score is Main$Score and static

    The specification makes every class of a compact source file a member of the implicitly declared class, which is why a plain class there is an inner class.

Start from a static nested class, and add the outer object only when needed

Four questions about the helper you need decide its kind. Answer them in the table: each answer rules kinds out or adds a caveat, with the reason, and the cells carry this lesson’s own classes:

Interactive · answer and compare Which kind of nested class fits your helper? Answer the four questions about the class you need. Each answer rules kinds out, with the reason.
Does the class need the fields or methods of one outer object?
Is it used in one method only?
Is it one method of an interface with a single abstract method, such as Comparator?
Does it need fields of its own?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to declare a helper class, against the four things that decide between them
Kind The outer object Local variables Fields and methods Who can use it
Static nested class Declared: static class inside a class Good none: it cannot keep an outer object alive by accident, like Builder Poor cannot read a method's local variables Good any fields and methods Good any code that may see it, as new Outer.Nested()
Inner class Declared: class inside a class, no static Mixed always one, kept in this$0 when used, as for Cursor and TitleLabel Poor cannot read a method's local variables Good any fields and methods Mixed code that has an outer object: outer.new Inner()
Local class Declared: inside a method or block Mixed the enclosing object, when declared in instance code Good reads final or effectively final ones, as Filter reads letters Good any fields and methods Poor only the block that declares it
Anonymous class Declared: new Interface() { … } in one expression Mixed the enclosing object, when written in instance code Good reads final or effectively final ones Mixed its own fields and methods; this is the object itself Poor one object, made where it is declared
Lambda Declared: (a, b) -> … for one method Mixed this means the enclosing object, as in the code around it Good reads final or effectively final ones Poor one method of a functional interface, no fields Poor one object, made where it is written
How the answers decide
  • “Does the class need the fields or methods of one outer object?” Yes, it reads its outer object: rules out Static nested class. A static nested class has no outer object; it would need one passed in, which an inner class does for you.
  • “Does the class need the fields or methods of one outer object?” No, it works on its own data: adds a caveat to Inner class. An inner object would keep an outer object reachable for nothing. Make it static unless it needs the outer object.
  • “Is it used in one method only?” No, in several places: rules out Local class, Anonymous class and Lambda. Local and anonymous classes and lambdas exist only where they are written, so other code cannot reuse them.
  • “Is it one method of an interface with a single abstract method, such as Comparator?” No, several methods, or a class to extend: rules out Lambda. A lambda implements exactly one abstract method of a functional interface, and cannot extend a class.
  • “Does it need fields of its own?” Yes, its own fields: rules out Lambda. A lambda has no fields of its own. An anonymous or local class can keep state between calls.
  • “Is it used in one method only?” Yes, in one method and “Is it one method of an interface with a single abstract method, such as Comparator?” Yes, one method and “Does it need fields of its own?” No fields: adds a caveat to Anonymous class and Local class. They work, but a lambda or a method reference says the same in less code, as the comparator section showed.

Local and anonymous classes and lambdas can also read the effectively final local variables around them; static nested and inner classes cannot.

Read the kinds from top to bottom as a preference: use a static nested class unless the class needs the outer object, an inner class when it does, a local class when only one method needs it, and an anonymous class or a lambda for a one-off object, preferring the lambda whenever the interface has one method.

Checkpoint

Check: Choosing the kind of a helper class

  1. Question 1 of 2 One sort call in one method needs to order songs by length. Which form fits best?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A lambda or a comparator built with Comparator.comparingInt

    Read the preference from the top: a lambda for one method of a functional interface used once, a static nested class for a reusable helper, an inner class only when the outer object is needed.

  2. Question 2 of 2 A one-off listener in one method must count how often it is called and implement both methods of an interface with two abstract methods, onOpen and onClose. Which form compiles and fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An anonymous class with a count field

    Fields of its own and several methods rule a lambda out; used in one place only, an anonymous or local class is the natural fit.

Practise: a range with an inner iterator

Range holds the integers from a start value, counting up by a step, while they stay below an end value. It is Iterable, so a for loop can walk it. Its Builder is a static nested class, and its iterator should be an inner class that reads the range’s fields:

range/Range.java Java · range/Range.java
import java.util.Collections;
import java.util.Iterator;

/**
 * The ints from start, counting up by step, while they stay below end:
 * Range.builder().from(1).to(10).step(3).build() holds 1, 4 and 7.
 */
final class Range implements Iterable<Integer> {
    private final int start;
    private final int end;
    private final int step;

    private Range(int start, int end, int step) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    static Builder builder() {
        return new Builder();
    }

    /** Your turn: return a new object of an inner class that walks start, start + step, ... while below end. */
    @Override
    public Iterator<Integer> iterator() {
        return Collections.emptyIterator();
    }

    /** Collects the three values, then builds the Range. The step starts at 1. */
    static final class Builder {
        private int start;
        private int end;
        private int step = 1;

        Builder from(int start) {
            this.start = start;
            return this;
        }

        Builder to(int end) {
            this.end = end;
            return this;
        }

        /** Your turn: keep the step, and refuse 0 or less with an IllegalArgumentException. */
        Builder step(int step) {
            return this;
        }

        Range build() {
            return new Range(start, end, step);
        }
    }
}
The sample checks against the starter class Java · range/RangeChecks.java
// Sample checks for Range. Edit Range.java, then run: java range/RangeChecks.java
int passed = 0;
int total = 0;

/** Runs one check; an exception counts as a failure, so every check gets its line. */
void check(String what, BooleanSupplier test) {
    total++;
    boolean ok;
    try {
        ok = test.getAsBoolean();
    } catch (RuntimeException e) {
        ok = false;
        what += " (threw " + e.getClass().getSimpleName() + ")";
    }
    if (ok) passed++;
    IO.println((ok ? "pass  " : "FAIL  ") + what);
}

List<Integer> values(Range range) {
    List<Integer> out = new ArrayList<>();
    for (int n : range) {
        out.add(n);
        if (out.size() > 100) break;      // a broken iterator must not run forever
    }
    return out;
}

boolean throwsIllegalArgument(Runnable action) {
    try {
        action.run();
        return false;
    } catch (IllegalArgumentException e) {
        return true;
    }
}

void main() {
    check("from 1 to 10 step 3 gives [1, 4, 7]",
            () -> values(Range.builder().from(1).to(10).step(3).build()).equals(List.of(1, 4, 7)));
    check("the step defaults to 1", () -> values(Range.builder().from(0).to(4).build()).equals(List.of(0, 1, 2, 3)));
    check("from 5 to 5 is empty", () -> values(Range.builder().from(5).to(5).build()).isEmpty());
    check("a step of 0 is refused", () -> throwsIllegalArgument(() -> Range.builder().step(0)));

    Range three = Range.builder().from(0).to(3).build();
    check("two iterators of one range move independently", () -> {
        Iterator<Integer> first = three.iterator();
        Iterator<Integer> second = three.iterator();
        first.next();
        first.next();
        return second.next() == 0 && first.next() == 2;
    });
    check("an exhausted iterator says hasNext false, and next throws NoSuchElementException", () -> {
        Iterator<Integer> it = three.iterator();
        while (it.hasNext()) it.next();
        try {
            it.next();
            return false;
        } catch (NoSuchElementException e) {
            return true;
        }
    });
    check("near Integer.MAX_VALUE it stops instead of wrapping around", () -> values(
            Range.builder().from(Integer.MAX_VALUE - 1).to(Integer.MAX_VALUE).step(5).build())
            .equals(List.of(Integer.MAX_VALUE - 1)));
    check("the iterator is an inner class of Range", () -> {
        Class<?> walker = three.iterator().getClass();
        return walker.getEnclosingClass() == Range.class && walker.isMemberClass()
                && !Modifier.isStatic(walker.getModifiers());
    });
    check("Builder is a static nested class", () -> Modifier.isStatic(Range.Builder.class.getModifiers()));

    IO.println(passed + " of " + total + " checks passed");
    if (passed < total) System.exit(1);
}

Output (exit status 1)

FAIL  from 1 to 10 step 3 gives [1, 4, 7]
FAIL  the step defaults to 1
pass  from 5 to 5 is empty
FAIL  a step of 0 is refused
FAIL  two iterators of one range move independently (threw NoSuchElementException)
pass  an exhausted iterator says hasNext false, and next throws NoSuchElementException
FAIL  near Integer.MAX_VALUE it stops instead of wrapping around
FAIL  the iterator is an inner class of Range
pass  Builder is a static nested class
3 of 9 checks passed

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java RangeChecks.java

Edit Range.java until java range/RangeChecks.java prints 9 of 9 checks passed:

  1. In Builder.step, throw an IllegalArgumentException for a step of 0 or less, and keep the step otherwise.
  2. Add a private final class inside Range that implements Iterator<Integer>, with a field for the next value. hasNext() is true while that value is below end; next() throws NoSuchElementException when there is nothing left, as the Iterator documentation requires, and otherwise returns the value and adds the step.
  3. Return a new object of that class from iterator(), so that two iterators never share a position.

The check near Integer.MAX_VALUE is the subtle one: adding the step to an int that is close to the maximum wraps around to a negative number, which is still below end. Keep the next value in a long.

Then go over the seven ideas of this lesson question by question, from the four kinds to the choice between them. Practice begins where your answers above went wrong most often, and the reason for each choice shows as you pick it:

Practice that adapts to you

Practise this lesson

14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 What kind of class is one declared with static inside another class?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: A static nested class

    static is what separates a static nested class from an inner class: no outer object, and no hidden field.

  2. Question 2 of 14 kinds/Main.java declares one anonymous Comparator. What is its class file called, according to the session?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: Main$1.class

    The listing shows Main$1.class beside Playlist$1Filter.class, Playlist$Builder.class and Playlist$Cursor.class: one class file for every class, nested or not.

  3. Question 3 of 14 How does code outside Screen create a TitleLabel that belongs to the screen home?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: home.new TitleLabel()

    Inside an instance method of Screen, plain new TitleLabel() passes this; outside, outer.new Inner() names the outer object.

  4. Question 4 of 14 Before JDK 18, what did javac generate for an inner class like Spacer, which never uses its outer object?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: The this$0 field too, used or not

    Leaving out unused this$0 fields lets outer objects be collected sooner, which is the point the release note makes.

  5. Question 5 of 14 When can the garbage collector reclaim an object?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: When no chain of references from the running program reaches it any more

    Code never frees objects itself; it can only stop referring to them. A hidden reference keeps that from happening.

  6. Question 6 of 14 Why does the Learn Java guide on nested classes strongly discourage serializing inner classes, local and anonymous ones included?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: The synthetic constructs the compiler adds for them can differ between compilers

    Another reason to start from a static nested class: it has no synthetic outer reference to serialize.

  7. Question 7 of 14 When is a local variable effectively final?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: When adding final to it would not cause a compile error

    limit = 4 and count++ both assign their variables again, which is why the session's two programs are refused.

  8. Question 8 of 14 A lambda must count how many times it has run, but count++ on a local int does not compile. Which change compiles and counts?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: Keep the count in an AtomicInteger and call incrementAndGet() in the lambda

    Capture a reference that never changes to an object that does: a field, an array element or an object such as an AtomicInteger.

  9. Question 9 of 14 What did the three sorts of comparator/Main.java print?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: The same order three times, Monsoon Song, Rain, Night Train Home

    Comparator.comparingInt(Song::seconds) states what is compared instead of how, which makes it the form to prefer.

  10. Question 10 of 14 In thisref/Screen.java, what does the anonymous Runnable print for this and for Screen.this?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: the anonymous Runnable, and Screen home

    The run printed anonymous: this is the anonymous Runnable; Screen.this is Screen home, then lambda: this is Screen home.

  11. Question 11 of 14 What does the specification say about a class declared in a compact source file?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: It is a member of the implicitly declared class of the file

    A plain class there is an inner class, while records, enums and interfaces are implicitly static.

  12. Question 12 of 14 StaticHelper.java creates new Tally() in a static method and does not compile. Which change fixes it?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Declare it static class Tally

    The other fix is to move Tally into a file of its own, as the examples of this module do once a program grows.

  13. Question 13 of 14 Which kind should a nested class be when it does not need the outer object?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: Static nested

    The first rule of choosing a kind: make a nested class static unless it really needs the outer object.

  14. Question 14 of 14 A Playlist must offer a read-only view of its own songs, one view object per playlist, used by many callers. Which kind fits?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: An inner class, because each view reads its own playlist's songs

    An inner class is the right tool exactly when the object needs its outer object, as a view of the playlist does.

Interview questions

Fresher · What is the difference between a static nested class and an inner class in Java?

A static nested class is declared with static inside another class and has no connection to any object of that class: you create it with new Outer.Nested(), and it can only reach the outer class’s instance members through a reference you give it. An inner class is declared without static, and every inner object belongs to one outer object: you create it with outer.new Inner(), or with plain new Inner() inside an instance method of the outer class, and its methods can read that outer object’s fields directly or as Outer.this. To make that work, the compiler passes the outer object to the inner constructor and keeps it in a hidden field when the inner class uses it. A good answer adds the consequence: an inner object keeps its outer object reachable, so prefer a static nested class unless the class really needs the outer object.

Two longer questions of this module start from this lesson: screens that stay in memory after they are closed, and the iterator of a ring buffer that must notice when the buffer changes.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A desktop app's screens each register an inner-class listener with an event bus that lives as long as the app. After an hour of opening and closing screens, a heap dump shows hundreds of closed screens still in memory. Explain the leak, how you would confirm it, and two ways to fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    You are writing a RingBuffer class and its iterator. Would you make the iterator an inner class or a static nested class, and how would you make it fail fast when the buffer changes while someone is iterating?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A nested class is static nested, inner, local or anonymous; inner, local and anonymous classes can see an enclosing object, and a static nested class cannot.
  • An inner class receives its outer object in its constructor and keeps it in a hidden this$0 field when it uses it; since JDK 18 the field is left out when the inner class does not.
  • An inner object keeps its outer object reachable, which is the usual cause of inner-class memory leaks, so make nested classes static unless they need the outer object.
  • Local and anonymous classes and lambdas can read only effectively final local variables, because they keep a copy of the value.
  • An anonymous class for a one-method interface is usually better as a lambda or method reference; inside a lambda, this is the enclosing object, not the lambda.
  • Every class declared in a compact source file is a member of the implicit class, so a plain class there is an inner class and cannot be created from a static method.

Terms in this lesson

anonymous class
A class without a name, declared and instantiated in one expression, as in new Comparator<String>() { … }. It implements an interface or extends a class, and javac names its class file Outer$1, Outer$2 and so on.
effectively final
Said of a local variable or parameter that is never changed after it is assigned, so adding final to it would not cause a compile error. Local and anonymous classes and lambdas may read only such variables or final ones.
enclosing instance
The object of the outer class that an inner object belongs to. The compiler passes it to the inner class's constructor and, when the inner class uses it, keeps it in a hidden field named this$0.
inner class
A nested class declared without static. Each of its objects belongs to one object of the outer class, its enclosing instance, which it can name as Outer.this; local and anonymous classes are inner classes too.
local class
A class declared inside a method or another block. It is known only there, and it may read the block's local variables and parameters that are final or effectively final.
nested class
A class declared inside another class or inside a method. It is static nested, inner, local or anonymous, and it may use the private members of the classes around it.
static nested class
A class declared with static inside another class. It has no enclosing object, so new Outer.Nested() needs none, and it reaches the outer class's instance members only through a reference it is given.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.