Topic 5.13
Nested, Inner, Local and Anonymous Classes
In one line
Java lets you declare a class inside another class, or even inside a method. A static nested class is an ordinary class that lives in another's namespace; an inner class is tied to one outer object through a hidden reference; local and anonymous classes live inside a block and can read effectively final local variables. Since Java 8, a lambda replaces most anonymous classes that implement a one-method interface.
Think of it like this
A school and its rooms. A room number like "Room 12" only makes sense inside one particular school building: it belongs to that school (an inner class belongs to one outer object). A school's rulebook, on the other hand, is printed once and applies to every school building; you can read it without standing in any building (a static nested class belongs to the class, not to an object). A substitute teacher who is called in for one lesson and leaves after it is like a local or anonymous class: they exist only inside one method, and they can see what was written on that classroom's board when they arrived.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Nested class
- Any class declared inside another class or interface. Its full name includes the outer class:
Playlist.Cursor. - Static nested class
- A nested class marked
static. It has no connection to any object of the outer class and can be created on its own. - Inner class
- A nested class without
static. Each of its objects is attached to one object of the outer class and can use that object's fields. - Enclosing instance
- The outer object an inner object belongs to. Inside the inner class you can name it as
Outer.this. - Local class
- A class declared inside a method or block. Only code later in that block can use its name.
- Anonymous class
- A class with no name, declared and created in a single expression such as
new Runnable() { public void run() { } }. - Effectively final
- A local variable that is never assigned again after it gets its first value, even though it isn't marked
final. - Capture
- When a local, anonymous class or lambda uses a local variable from the method around it. Java copies the value into the new object.
- Functional interface
- An interface with exactly one abstract method, like
RunnableorComparator. A lambda can stand in for it. - Synthetic
- Generated by the compiler and not written in your source, like the hidden
this$0field of an inner class.
Step by step
01Why put a class inside a class at all?
Some classes only make sense as part of another one. A linked list's Node is useless outside the list. A Cursor that walks a playlist only makes sense for one playlist. Putting them inside the owner does three things: it groups related code in one file, it hides the helper (a private nested class is invisible outside the outer class), and it gives the helper access to the outer class's private members.
Java has four flavours, and the difference between them is one question: *does the nested object need an outer object?* If no, use static. If yes, use an inner class. If the class is needed in only one method, declare it there (local) or inline it with no name (anonymous).
02Static nested classes: a normal class with a longer name
Mark a nested class static and it behaves exactly like a top-level class, except for its name (IntList.Node) and its access: it may use the outer class's private members, and the outer class may use its private members too.
There is no outer object involved. new IntList.Node(5) works from anywhere the class is visible, with no IntList in sight. If a static nested class wants an outer object's fields, it must be given an object and use it explicitly (list.head), just like any other class.
Builders are the textbook example: Pizza.Builder is created before any Pizza exists, so it *must* be static. See the System Design course's Builder pattern (/topic/phase-2/builder).
class IntList {
private static class Node { // private: callers never see it
final int value;
Node next;
Node(int value) { this.value = value; }
}
private Node head; // the outer class uses Node freely
void push(int v) {
Node n = new Node(v); // no IntList.this needed to build a Node
n.next = head;
head = n;
}
}03Inner classes: each object carries its outer object
Remove static and the class becomes an inner class. Now every inner object is born attached to an outer object, and its code can use the outer object's fields and methods by their plain names.
Creating one from outside uses an unusual syntax that makes the attachment visible: Playlist.Cursor c = road.new Cursor();. Inside an instance method of Playlist, plain new Cursor() works because this is supplied for you.
If an inner field shadows an outer field with the same name, name means the inner one and Playlist.this.name reaches the outer one. Since Java 16 (JEP 395, which finalised records) an inner class may also declare static members; before Java 16 that was a compile error except for compile-time constants.
04What javac really generates
The JVM doesn't understand nesting. javac writes each nested class to its own file, named Outer$Inner.class, and rewrites inner classes as plain classes with an extra synthetic field and constructor parameter. javap -p (the class-file disassembler that ships with the JDK) shows them.
Playlist$Cursor gets a field final Playlist this$0 and its constructor takes a Playlist. So road.new Cursor() is compiled as roughly new Playlist$Cursor(road) (with a null check on road), and songs inside it becomes this$0.songs. Playlist$Stats, the static one, has no such field.
Two newer details: since Java 11 (nestmates, JEP 181) the outer and nested classes are recorded as one 'nest', so private access between them is checked by the JVM directly; older compilers generated hidden bridge methods named access$000. And recent javac (JDK 18 onwards) leaves out this$0 when the inner class never uses the outer instance, though the constructor still takes it, so the class is still tied to an outer object at creation.
05Local classes: a class that lives in one method
You can declare a class inside a method body, after which the rest of that block can use it like any class. Nobody outside the method can even name it. It may implement interfaces, extend classes and have several methods and fields.
A local class declared in an instance method has an enclosing instance (like an inner class). It can also read the method's local variables and parameters, as long as they are final or effectively final. javac names it Main$1Polite.class: the number keeps two local classes called Polite in different methods apart.
Since Java 16 you can also declare local records, enums and interfaces. Those are implicitly static, so they can't use the method's local variables or this.
06Anonymous classes: declare and create in one go
An anonymous class is an expression: new SomeInterface() { ...body... } or new SomeClass(args) { ...body... }. It declares a nameless class that implements the interface (or extends the class) and creates exactly one object of it, right there.
It can't have a constructor (it has no name to give one), but it can pass arguments to its parent's constructor, and it can have fields, extra methods and an instance initializer block { ... }. It can only implement one interface or extend one class. Since Java 9 you may use the diamond with it: new Comparator<>() { ... }.
javac names anonymous classes by number in source order: Main$1, Main$2, ... Stack traces show these names, which is one reason anonymous code is harder to debug than named classes.
07Capturing locals: why they must be effectively final
Local variables live in the method's stack frame, which is thrown away when the method returns. An anonymous object can live on (stored in a list, run on another thread later). So Java does not let the object point at the variable; it copies the value into a synthetic field (javap shows final java.lang.String val$greeting).
With a copy, a later assignment to the original would be invisible to the copy, and vice versa. To avoid two silently different values, the compiler requires captured locals to be final or effectively final. Change the variable anywhere, before or after the class, and you get local variables referenced from an inner class must be final or effectively final.
Fields are different: an inner class reaches a field *through the outer object*, so it always sees the current value and may change it. If you really need a mutable counter shared with an anonymous class or lambda, use a field, an AtomicInteger, or a one-element array (the last is a hack; prefer the first two).
08When a lambda replaces an anonymous class
Most anonymous classes in old code implement a functional interface: Runnable, Comparator, ActionListener. Since Java 8, a lambda says the same thing in one line: words.sort((a, b) -> a.length() - b.length()), or better, Comparator.comparingInt(String::length). Lambdas get their own full topic later in this course.
They are not identical. Inside a lambda, this means the enclosing object; inside an anonymous class, this is the anonymous object itself. A lambda can't have fields, so it can't keep its own state. A lambda can't redeclare a local variable name from the enclosing method; an anonymous class can shadow it. And a lambda can only implement an interface with one abstract method, never an abstract class.
Under the hood, a lambda is not an inner class. javac puts the body in a private method (private static int lambda$main$0(String, String)) and emits an invokedynamic instruction. The first time it runs, LambdaMetafactory creates a small class in memory (a hidden class since Java 15). No Main$2.class file appears, and a lambda that doesn't use this doesn't hold a reference to the outer object, so it can't cause the outer-object leak described above.
Try it yourself
- 1
Predict the class files
Before compiling the 'Local and anonymous classes' example, write down the
.classfiles you expect. Then add a second anonymousGreeteraftershoutand predict its name. Runjavac Main.javaand list the folder: you should seeMain$2.classappear. - 2
Break the capture rule
In the same example, add the line
greeting = "Hi";at the very end ofmain, after the loop. Predict whether it compiles. It doesn't: the assignment makesgreetingno longer effectively final, even though it happens after the classes are declared. - 3
Fix the leak
In the memory-leak example, change
ClickHandlerso it no longer needs the screen: make itstaticand give it only the data it needs (ClickHandler(int size)). Create it withnew Screen.ClickHandler(10_000_000). Now nothing in the registry reaches theScreenobject, so it can be collected oncescreen = null. - 4
Turn the anonymous class into a lambda
In the comparator example, replace the anonymous
ComparatorwithComparator.comparingInt(String::length). Predict the output first: it must be the same first line, because both sort by length andList.sortis stable.
Code & diagrams
Expected output
Road trip: Song A
Road trip: Song B
owner is road? true
2 songsThe class names printed are the same names javac gives the .class files.
Expected output
Hello, dear Asha
HELLO Asha!
Main$1Polite
Main$1
anonymous? true
local? trueSetting screen to null did not free the 10 MB: the registry reaches the handler, and the handler's hidden this$0 reaches the Screen.
Expected output
clicked a screen of 10000000 bytes
clicked, no screen keptExpected output
anonymous: this = the anonymous object, calls = 1
anonymous: Main.this = the Main object
anonymous: this = the anonymous object, calls = 2
anonymous: Main.this = the Main object
lambda: this = the Main objectEach sort starts from the previous result. The last one sorts by length, then alphabetically when lengths are equal.
Expected output
anonymous : [fig, kiwi, apple, banana]
lambda : [kiwi, fig, banana, apple]
comparing : [fig, kiwi, apple, banana]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Create an inner class from static main
Declare a non-static member class Counter inside Main and write new Counter() in main.
public class Main {
class Counter { int n; }
public static void main(String[] args) {
Counter c = new Counter();
}
}Break #2
Modify a captured local in an anonymous class
Increment a local count from inside an anonymous Runnable.
public class Main {
public static void main(String[] args) {
int count = 0;
Runnable r = new Runnable() {
public void run() { count++; }
};
}
}Break #3
Capture a loop counter in a lambda
Inside for (int i = 0; i < 3; i++), add () -> System.out.println("task " + i) to a list.
for (int i = 0; i < 3; i++) {
tasks.add(() -> System.out.println("task " + i));
}Myth vs fact
Myth
A static nested class can only have static members.
Fact
static on a nested class only means 'no outer instance'. The class can have instance fields and methods and you can create many objects of it, like any other class.
Myth
Lambdas are just shorter anonymous classes.
Fact
They compile differently (a private method plus invokedynamic, no .class file), this means the enclosing object, they can't hold state or shadow locals, and a non-capturing lambda is typically a single reused object.
Myth
Captured variables are passed by reference, so the inner class sees later changes.
Fact
The value is copied when the object is created. That is exactly why Java requires the variable to be effectively final: so the copy can never be out of date.
Myth
Inner classes are a JVM feature.
Fact
They are a compiler feature. The JVM sees ordinary classes named Outer$Inner with a synthetic this$0 field. Only the nest attributes (Java 11) and the InnerClasses attribute record the relationship for access checks and reflection.
Interview problem
The problem
Tasks created in a loop
An interviewer shows: List<Runnable> tasks = new ArrayList<>(); for (int i = 0; i < 3; i++) { tasks.add(() -> System.out.println("task " + i)); } and asks: what does running every task print? Fix it, and explain what the language is protecting you from.
The interviewer follows up
Why can a lambda modify a field but not a local?
Does the lambda in the fixed version hold a reference to the enclosing object?
How would you share a running counter across these tasks?
When it breaks
Listener registered as an inner or anonymous class leaks its screen or request
What you see
Heap grows after each page/screen/request; a heap dump shows thousands of Screen or Android Activity objects reachable only through Screen$1.this$0 from a static event bus or listener list. Eventually OutOfMemoryError: Java heap space.
Fix & prevent
Unregister listeners in a lifecycle hook (close(), onDestroy). Use static nested classes or lambdas that don't capture this, and hold the outer object through a WeakReference if the listener must outlive it. Confirm with a heap dump's 'path to GC roots'.
Callback submitted to a long-running executor captures a large outer object
What you see
A scheduled task built as an anonymous Runnable inside a request handler keeps the whole handler (and its request body, buffers, DB results) alive until the task runs or forever if it repeats. Memory looks like a slow leak under load.
Fix & prevent
Extract the work into a static nested class or a lambda that captures only the few values it needs (copy them into locals first). Review ScheduledExecutorService tasks for accidental this capture.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Double-brace initialization (
new HashMap<>() {{ put("a", 1); }}) creates an anonymous subclass per use site with an instance initializer. In an instance context it can capture the enclosing instance (always with javac before JDK 18, and whenever the initializer uses outer members), a leak risk, adds a class file each time, and breaksequalsthat comparesgetClass(). UseMap.of(...)(Java 9) or a plain builder instead. - ▸
Serialization: serializing an inner or anonymous class object also tries to serialize its
this$0outer object (and capturedval$fields), which often fails withjava.io.NotSerializableException: <OuterClass>or drags in far more data than intended. The serialization spec strongly discourages serializing inner classes, and their synthetic field names are compiler-specific. Use static nested classes for anything serializable. - ▸
Interface members are implicitly
public static, so a class declared inside an interface is always a static nested class. Memberenums,records andinterfaces are implicitlystaticeverywhere, which is why a nested record never holdsthis$0. - ▸
Before Java 11, an inner class touching an outer
privatefield went through a compiler-generated package-private bridge (access$000), which widened the effective access and added a call. With nestmates (JEP 181) the class files carryNestHost/NestMembersattributes and the JVM allows private access inside the nest directly; reflection gainedClass.getNestHost()andisNestmateOf().
Remember this
- 1
A nested class is any class declared inside another class or interface. There are four kinds. A static nested class (
static class Node { }inside a class) has no link to any outer object. An inner class (the same, withoutstatic) is attached to one outer object. A local class is declared inside a method body, like a local variable. An anonymous class has no name: it is declared and created in one expression,new Runnable() { ... }. Inner, local and anonymous classes together are called inner classes in the Java Language Specification, because they can all have an enclosing instance. - 2
A static nested class is just a normal top-level class that lives inside another class's name. You write
Playlist.Statsfrom outside and create it withnew Playlist.Stats(), noPlaylistobject needed. It can read the outer class'sprivate staticmembers, and it can read private instance fields *of an object it is handed*, but it has nothisof the outer class. Use it for helpers that belong to one class: a linked list'sNode, aBuilder, aMap.Entrystyle pair. - 3
An inner class (non-static member class) always carries a hidden reference to its outer object, written
Outer.thisin source. That is why inner code can read outer fields directly:songs.size()insideCursorreally meansPlaylist.this.songs.size(). Because of that reference you can only create one *from an outer object*:road.new Cursor(), or plainnew Cursor()inside an outer instance method wherethisis available. From astaticmethod there is nothis, and the compiler saysnon-static variable this cannot be referenced from a static context. - 4
That hidden reference is also the classic memory-leak pitfall. As long as anything reaches the inner object, the outer object cannot be garbage collected, even if nothing else uses it. (Recent
javac, JDK 18 onwards, drops the field when the inner code never touches the outer object; older compilers always kept it, and any inner class that does use outer fields always keeps it.) Register an inner-class listener with a long-lived registry and the whole outer object, with every field it holds (a 10 MB image, a database connection, an AndroidActivity), stays in memory. **Rule of thumb from Effective Java (Item 24): if a member class does not need the outer instance, make itstatic.** - 5
Local and anonymous classes can read local variables and parameters of the enclosing method, but only if they are final or effectively final: never assigned after their first value (Java 8 added the 'effectively' part; before that you had to write
final). The reason is copying: the compiler copies each captured value into a hidden field of the inner object (val$greeting), because the object may outlive the method's stack frame. If the variable could change later, the copy and the original would disagree, so Java forbids the change instead of surprising you. - 6
javacturns every nested class into its **own.classfile** named with a$:Playlist$Cursor.classfor a member class,Main$1Polite.classfor a local class (number plus name), andMain$1.class,Main$2.classfor anonymous classes, numbered in source order. The JVM has no real 'inner class' concept; the outer reference is a synthetic constructor parameter and afinalfieldthis$0. A lambda (Java 8) is different: no.classfile at all, its body becomes a private methodlambda$main$0in the outer class, and aninvokedynamiccall builds the object at run time. For a functional interface (exactly one abstract method), prefer a lambda; keep an anonymous class when you need state, more than one method, an abstract class, orthisto mean the object itself.
Explain it without notes
What are the four kinds of nested classes, and how do they differ?
Why can an inner class cause a memory leak, and how do you avoid it?
Why must captured local variables be final or effectively final?
How does javac compile nested classes and lambdas? Name the files and synthetic members.
When would you still choose an anonymous class over a lambda?
Practice
Write an IntList singly linked list whose Node is a private static nested class. Add add(int) and a toString() that prints [3, 1, 4].
Write Range implements Iterable<Integer> whose iterator is a private inner class reading the outer from and to fields. Loop over new Range(2, 6) with an enhanced for.
Using an anonymous class, create a Supplier<Integer> ticket machine that returns 1, 2, 3 on successive calls. Explain why a plain lambda can't hold that counter itself.
Build three Runnables in a for (int i ...) loop that print task 0, task 1, task 2, plus two more from an enhanced for over List.of("x", "y").
Trade-offs
- ↔
Static nested vs inner: static is cheaper (no hidden reference, no leak risk, can be created anywhere), inner is more convenient when the helper truly works on one outer object's state, like an iterator.
- ↔
Nested vs top-level: nesting keeps a helper private and close to its only user, but large nested classes make the outer file long and hard to navigate. Promote a nested class to top level once other classes need it.
- ↔
Anonymous class vs lambda: anonymous classes can hold state, implement abstract classes and use
thisfor themselves, but they are verbose, add a class file each, and keep the enclosing instance alive whenever they use it (with javac before JDK 18, even when they don't). Lambdas are concise and capture only what they use, but give less readable stack traces. - ↔
Local class vs anonymous class: a local class can be instantiated several times, have a constructor and a name in stack traces; an anonymous class is shorter for a single use.
Done when you can
Done when you can name the four kinds of nested classes and say which ones have an enclosing instance.
Done when you can create an inner object from outside (
outer.new Inner()) and reach a shadowed outer field withOuter.this.Done when you can explain and demonstrate the inner-class memory leak and fix it with a static nested class.
Done when you can explain effectively final capture and fix the loop-variable compile error.
Done when you can predict the
.classfile names javac produces and describethis$0andval$x.Done when you can turn an anonymous class into a lambda and list the cases where you shouldn't.