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.
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:
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);
} 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
Builderis 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 ofPlaylist. Builders and small helper types are the typical static nested classes.Cursoris inner.iterator()returnsnew Cursor(), and that cursor reads thesongsfield of the very playlist that created it. Theforloop over a playlist uses one, without ever naming the class.Filteris local. It exists only insidelongerThan, and it reads the method’s parameterletters.- The comparator in
Mainis 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
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:
/** 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";
}
}
} // 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
TitleLabelreadstitle, so it has the fieldfinal 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.Spacernever touches its screen, and it has no field at all. The JDK 18 release notes describe the change: before JDK 18,javacalways generated the field; since then it omits it when the inner class does not use its enclosing instance. The constructor still takes aScreen, sonew Spacer()still needs one.Styleis static nested: no field, and a constructor without aScreenparameter.
Step through the second half of the program, where the label is created and asked for its text:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- An outer object first. main creates the screen with new Screen("Home"). An inner object always needs one, so this comes first.
- 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.
- The hidden parameter. The compiler gave the constructor a parameter for the outer object, Screen$TitleLabel(Screen), as reflection lists. home arrives through it.
- The hidden field. Because TitleLabel reads title, the constructor stores home in the synthetic field this$0. The run confirms it holds home: true.
- text() reads through it. label.text() reads title through this$0, which is how an inner object reads its outer object's private field.
- 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
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:
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.
- A long-lived, static list of listeners holds a TitleLabel object.
- 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.
- 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.
- 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The screen is open. The app refers to the screen, and the screen is in use.
- 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.
- The screen is closed. The app drops its reference. Nothing that the app holds leads to the screen any more.
- 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.
- 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
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.
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();
} 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
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().:
// 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.
/** 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;
}
} 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
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:
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);
} 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
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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:
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);
}
}
} // 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:
- In
Builder.step, throw anIllegalArgumentExceptionfor a step of 0 or less, and keep the step otherwise. - Add a
private final classinsideRangethat implementsIterator<Integer>, with a field for the next value.hasNext()istruewhile that value is belowend;next()throwsNoSuchElementExceptionwhen there is nothing left, as theIteratordocumentation requires, and otherwise returns the value and adds the step. - 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.
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.
-
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.
-
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?
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$0field 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
staticunless 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,
thisis 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
- The Java Language Specification, Java SE 25: 8.1.3 Inner Classes and Enclosing Instances (Oracle)
- The Java Language Specification, Java SE 25: 8.1.8 Implicitly Declared Classes (Oracle)
- The Java Language Specification, Java SE 25: 14.3 Local Class and Interface Declarations (Oracle)
- The Java Language Specification, Java SE 25: 15.9.5 Anonymous Class Declarations (Oracle)
- The Java Language Specification, Java SE 25: 15.27.2 Lambda Body (Oracle)
- The Java Language Specification, Java SE 25: 4.12.4 final Variables (Oracle)
- JDK 18 Release Notes: Enclosing Instance Fields Omitted from Inner Classes That Do Not Use Them (Oracle)
- Nested Classes (Learn Java) (Oracle)
- Interface Iterator (Java SE 25) (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress