Lesson 18 • Advanced
Inner Classes & Anonymous Classes
Learn Java's four kinds of nested class — static nested, non-static inner, local, and anonymous — when each one fits, and when a lambda is the better tool.
What You'll Learn in This Lesson
- ✓Write a static nested class as a self-contained helper
- ✓Write a non-static inner class and reach its enclosing instance
- ✓Use Outer.this to refer to the enclosing object explicitly
- ✓Declare local classes inside a method and capture variables
- ✓Replace verbose anonymous classes with concise lambdas
- ✓Avoid the memory leak that a hidden outer reference can cause
📚 Before You Start
This lesson builds on a few earlier ones. You should be comfortable with:
- Classes & objects — fields, constructors, and
this. - Interfaces — implementing a contract like
ComparatororRunnable. - Lambdas — the short form for a single-method interface.
🏠 A Real-World Analogy: Rooms and Buildings
Picture a house. A non-static inner class is a room inside the house — it can see the house's address and shares its plumbing, and it cannot exist without the house around it. A static nested class is a detached garage in the same plot: grouped with the house and built by the same architect, but it stands on its own and knows nothing about who lives inside.
A local class is a pop-up tent you put up for one job and take down when the job is done — it exists only for the length of one method. An anonymous class is a one-time contractor you hire on the spot without a name on the books. And a lambda is sending a quick text instead of hiring anyone at all — the right move when the job is a single small task.
1️⃣ Static Nested Classes — Grouped, but Independent
A static nested class is a class declared inside another class and marked static. The static keyword here does not mean "shared" the way a static field does — it means "no link to an enclosing instance". It is really just a normal top-level class that lives inside another for organisation, so you create it with the outer name as a prefix and no outer object is needed.
Use it for helpers that conceptually belong to the outer class but do not need any of its instance data — a Point inside a Shape, a Result inside a Calculator, or the classic Builder inside the thing it builds. Read the comments below.
public class Main {
// A STATIC nested class has NO link to a Main instance.
// It is just a top-level class that happens to live inside Main,
// so it can be a self-contained helper grouped with its owner.
static class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
double distanceTo(Point other) {
int dx = x - other.x;
int dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy); // straight-line distance
}
@Override public String toString() { return "(" + x + ", " + y + ")"; }
}
public static void main(String[] args) {
// Create it with the OUTER name as a prefix — no Main object needed.
Point a = new Main.Point(0, 0);
Point b = new Point(3, 4); // inside Main, the prefix is optional
System.out.println("a = " + a); // (0, 0)
System.out.println("b = " + b); // (3, 4)
System.out.println("distance = " + a.distanceTo(b)); // 5.0
}
}a = (0, 0)
b = (3, 4)
distance = 5.02️⃣ Non-static Inner Classes — Tied to an Instance
A non-static inner class (also called a member class) drops the static keyword. Now every inner instance is bound to one specific instance of the enclosing class and carries a hidden reference to it. That lets the inner class read and write the outer object's fields directly, as if they were its own.
Because an inner instance needs an enclosing instance, you cannot write new Inner() from outside — you write outer.new Inner(). When a name in the inner class is ambiguous, the qualified form Outer.this.field always means "the field on the enclosing object".
public class Main {
static class Counter {
private int count = 0; // a field of the OUTER object
void increment() { count++; }
// A NON-STATIC inner class. Each Ticker is tied to ONE Counter
// instance and can read/write that Counter's fields directly.
class Ticker {
// No 'count' here — it reaches OUT to the enclosing Counter.
String report() {
// 'count' means Counter.this.count — the outer instance.
return "count is now " + count;
}
void bump() { count++; } // mutates the outer object
}
// Build an inner instance from a Counter: outer.new Inner()
Ticker newTicker() { return new Ticker(); }
}
public static void main(String[] args) {
Counter c = new Counter();
c.increment(); // count = 1
// An inner instance needs an enclosing instance to exist.
Counter.Ticker t = c.newTicker(); // same as: c.new Ticker()
System.out.println(t.report()); // count is now 1
t.bump(); // inner class changes the OUTER field
System.out.println(t.report()); // count is now 2
}
}count is now 1
count is now 2Ticker, the bare name count is shorthand for Counter.this.count. The inner class has no count field of its own — it is reaching out to the enclosing Counter.🎯 Your Turn #1: Build an Inner Instance
A non-static inner instance must be created from its enclosing object. Fill in the blank so Car is built from the Garage you already have. The expected output is in the comment — run it and check.
public class Main {
static class Garage {
private String owner = "Sam";
// Non-static inner class: reaches the outer Garage's 'owner'.
class Car {
String describe() { return owner + "'s car"; }
}
}
public static void main(String[] args) {
// 🎯 YOUR TURN — fill in the blanks marked with ___
Garage g = new Garage();
// 1) Build a Car. An inner instance needs its enclosing Garage.
// 👉 replace ___ with the enclosing instance (the variable that is a Garage)
Garage.Car car = ___.new Car();
System.out.println(car.describe());
// ✅ Expected output:
// Sam's car
}
}3️⃣ Local Classes, Anonymous Classes, and Lambdas
A local class is declared inside a method and is visible only there. It is handy when a small helper is needed in exactly one place. Like inner classes, it can use local variables from the surrounding method — but only ones that are effectively final (assigned once and never changed), because the value is captured by copy.
An anonymous class goes one step further: it is an unnamed class defined and instantiated in a single expression. The classic use was implementing a one-method interface like Comparator on the spot. Today a lambda does the same job with far less noise — so for any functional interface (exactly one abstract method), prefer the lambda. Keep anonymous classes for the cases a lambda cannot cover: extra fields, multiple methods, or extending a class.
import java.util.Arrays;
import java.util.Comparator;
public class Main {
record Word(String text) {}
public static void main(String[] args) {
String prefix = "len="; // effectively final — usable below
// LOCAL CLASS: declared inside a method, only visible in this method.
// It can capture 'prefix' because that variable never changes.
class Labeller {
String label(Word w) { return prefix + w.text().length(); }
}
Labeller labeller = new Labeller();
System.out.println(labeller.label(new Word("hello"))); // len=5
Word[] words = { new Word("pear"), new Word("fig"), new Word("cherry") };
// ANONYMOUS CLASS: an unnamed one-off implementation of Comparator.
Comparator<Word> byLengthAnon = new Comparator<Word>() {
@Override public int compare(Word a, Word b) {
return Integer.compare(a.text().length(), b.text().length());
}
};
// LAMBDA: the same single-method interface, far less boilerplate.
Comparator<Word> byLengthLambda =
(a, b) -> Integer.compare(a.text().length(), b.text().length());
Arrays.sort(words, byLengthAnon);
System.out.print("anon: ");
for (Word w : words) System.out.print(w.text() + " ");
System.out.println();
Arrays.sort(words, byLengthLambda);
System.out.print("lambda: ");
for (Word w : words) System.out.print(w.text() + " ");
System.out.println();
}
}len=5
anon: fig pear cherry
lambda: fig pear cherry🎯 Your Turn #2: Anonymous Class → Lambda
Runnable is a functional interface — one method, run(), with no arguments and no return value. Rewrite the verbose anonymous class as a lambda by filling in the two blanks. Compare with the expected output in the comment.
public class Main {
public static void main(String[] args) {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// The verbose anonymous-class way (already written, for reference):
Runnable verbose = new Runnable() {
@Override public void run() { System.out.println("verbose ran"); }
};
verbose.run();
// 1) Write the SAME Runnable as a lambda. Runnable.run() takes no
// arguments and returns nothing, so the lambda has empty () params.
// 👉 replace the first ___ with empty parentheses: ()
// 👉 replace the second ___ with the body that prints "lambda ran"
Runnable concise = ___ -> ___;
concise.run();
// ✅ Expected output:
// verbose ran
// lambda ran
}
}4️⃣ Choosing the Right Kind
Default to a static nested class. Only drop static when the helper truly needs to reach into the enclosing instance, and reach for a lambda whenever you are implementing a single-method interface.
| Kind | Outer instance? | Reach for it when… |
|---|---|---|
| static nested | ❌ none | Self-contained helper grouped with its owner (Builder, Result, Node*). |
| non-static inner | ✅ required | Helper that must read/write the enclosing object's fields. |
| local class | ✅ + final locals | A named, possibly stateful helper used in one method only. |
| anonymous | ✅ + final locals | A one-off that a lambda can't express (fields / several methods). |
| lambda | enclosing this | Implementing a functional interface — the default modern choice. |
* A linked-list Node is usually static nested: it carries its own data and doesn't need the list instance.
5️⃣ Real-World Example: The Inner-Class Memory Leak
Here is the leak in miniature. A long-lived EventBus keeps a callback alive for the whole program. If that callback is a non-static inner Handler, it secretly pins the entire Screen it came from, so the garbage collector can never reclaim the screen. The static SafeHandler holds only the small String it needs, so the big object is free to go. Same output — very different memory behaviour.
public class Main {
// A registry that keeps callbacks alive for the program's lifetime.
static class EventBus {
// For the demo we just store one handler.
Runnable handler;
void register(Runnable r) { handler = r; }
void fire() { if (handler != null) handler.run(); }
}
// ❌ LEAKY: a non-static inner class. Every Screen.Handler secretly holds
// a reference to its Screen, so registering one keeps the WHOLE Screen
// (and everything it owns) alive as long as the EventBus lives.
static class Screen {
String name;
Screen(String name) { this.name = name; }
class Handler implements Runnable {
public void run() { System.out.println("handling for " + name); }
}
Runnable leakyHandler() { return new Handler(); }
}
// ✅ SAFE: a static nested class holds NO outer reference. Pass in only the
// small piece of data it needs, so the big object can be collected.
static class SafeHandler implements Runnable {
private final String label; // just a String, not the whole Screen
SafeHandler(String label) { this.label = label; }
public void run() { System.out.println("handling for " + label); }
}
public static void main(String[] args) {
EventBus bus = new EventBus();
Screen screen = new Screen("Home");
bus.register(screen.leakyHandler()); // bus now pins the entire Screen
bus.fire(); // handling for Home
bus.register(new SafeHandler("Home")); // bus pins only a String
bus.fire(); // handling for Home
}
}handling for Home
handling for HomeMini-Challenge: An Iterable Bag
Time to fly solo. The starter below has only a comment outline — no filled-in logic. Add a non-static inner class that implements Iterator<String> and reads the enclosing Bag's items and size fields, then return one from iterator(). The expected output is in the comments.
import java.util.Iterator;
public class Main {
static class Bag implements Iterable<String> {
private final String[] items;
private int size = 0;
Bag(int capacity) { items = new String[capacity]; }
void add(String s) { items[size++] = s; }
// 🎯 MINI-CHALLENGE: a non-static inner Iterator
// 1. Declare a NON-STATIC inner class 'BagIterator' that
// implements Iterator<String>.
// 2. Give it an int field 'pos' starting at 0.
// 3. hasNext() -> return pos < size (reads the OUTER 'size')
// 4. next() -> return items[pos++] (reads the OUTER 'items')
// 5. Make iterator() return a new BagIterator().
//
// ✅ Expected output (for the main below):
// a b c
public Iterator<String> iterator() {
// your code here — return a new inner iterator
}
}
public static void main(String[] args) {
Bag bag = new Bag(3);
bag.add("a"); bag.add("b"); bag.add("c");
for (String s : bag) System.out.print(s + " ");
System.out.println();
}
}Common Errors (and How to Fix Them)
- ❌ Memory leak from a hidden outer reference: a non-static inner callback (a
Runnable, listener, orHandler) registered with a long-lived object keeps its entire enclosing instance alive — the GC can't reclaim it. Fix: make the helper astaticnested class and pass in only the data it needs. - ❌ Confusing static nested with non-static inner: writing
new Outer.Inner()for a non-static inner class fails withan enclosing instance that contains Outer.Inner is required. A non-static inner needsouter.new Inner(); only astaticnested class can be created without an enclosing object. - ❌ Adding
staticmembers to a non-static inner class: before Java 16 a member inner class can't declarestaticfields or methods —Illegal static declaration in inner class. Make the nested classstaticif it needs static members. - ❌ Capturing a changing local variable:
local variables referenced from an inner class must be final or effectively final. A local/anonymous class or lambda may only capture variables that are never reassigned. Hold mutable state in a one-element array or anAtomicIntegerand change its contents instead. - ❌ An anonymous class where a lambda fits: writing
new Comparator<>() { ... }for a one-method interface is needless boilerplate and introduces a confusing newthis. For any functional interface, prefer the lambda(a, b) -> ....
📋 Quick Reference
| Goal | Code | Notes |
|---|---|---|
| Declare static nested | static class Inner { … } | No outer instance |
| Create static nested | new Outer.Inner() | No object needed |
| Declare inner | class Inner { … } | Holds outer ref |
| Create inner | outer.new Inner() | Needs an enclosing object |
| Refer to outer | Outer.this.field | Disambiguates a shadowed name |
| Local class | class Helper { … } inside a method | Captures effectively-final locals |
| Anonymous class | new Iface() { … } | One-off; needs fields/methods |
| Lambda | (a, b) -> a + b | Functional interface — default choice |
❓ Frequently Asked Questions
What is the difference between a static nested class and a non-static inner class?
A static nested class is declared with the static keyword and has no link to an instance of the enclosing class — it is really just a top-level class scoped inside another for organisation, and you create it with new Outer.Nested(). A non-static inner class (also called a member class) is tied to one specific enclosing instance, holds a hidden reference to it, can read and write that instance's fields directly, and must be created from an enclosing object with outer.new Inner(). Rule of thumb: make it static unless it genuinely needs the outer instance.
How does a non-static inner class reach the outer object, and what is Outer.this?
Every non-static inner instance stores a hidden reference to the enclosing instance that created it. When you use an outer field or method name inside the inner class, the compiler resolves it against that enclosing instance. If a name is shadowed (the inner class has a field of the same name), you disambiguate with the qualified form Outer.this.fieldName, which explicitly means 'the field on the enclosing object'.
How can an inner class cause a memory leak?
Because a non-static inner class holds an implicit reference to its outer instance, anything that keeps the inner object alive also keeps the entire outer object (and everything it references) alive. This bites you when the inner object outlives the outer — for example a Runnable, listener, or callback registered with a long-lived bus or executor. The garbage collector cannot reclaim the outer object while the callback exists. The fix is to make the helper a static nested class and pass in only the small piece of data it actually needs.
When should I use a lambda instead of an anonymous class?
Use a lambda whenever you are implementing a functional interface — an interface with exactly one abstract method, like Runnable, Comparator, or Callable. The lambda is shorter, has no separate class file, and does not introduce a new 'this' (its 'this' is the enclosing instance, not the lambda object). Reach for an anonymous class only when you need something a lambda cannot express: implementing an interface with several methods, extending a class, declaring instance fields, or overriding more than one method.
What is a local class and why must captured variables be effectively final?
A local class is declared inside a method (or constructor or block) and is visible only there. Like anonymous classes and lambdas, it can capture local variables from the surrounding method, but only ones that are effectively final — assigned once and never changed. Java copies the captured value into the class instance, so allowing later changes would let the two copies disagree. If you genuinely need mutable captured state, hold it in a single-element array or an AtomicInteger and mutate the contents rather than the variable.
🎉 Lesson Complete!
You can now choose between Java's four nested classes with confidence: a static nested class for an independent helper, a non-static inner class when you need the enclosing instance (created with outer.new Inner()), a local class for one method, and an anonymous class only when a lambda can't do the job. You also know how a hidden outer reference leaks memory — and how to stop it.
Next: Exception Handling Architecture — custom hierarchies, chained exceptions, and error strategies.
Sign up for free to track which lessons you've completed and get learning reminders.