Inner Classes and Anonymous Classes
Classes inside classes. Static nested, inner, local, anonymous, lambdas — the spectrum from explicit to invisible, and when each is the right tool.
Classes inside classes
Java lets you declare a class inside another class. There are four flavours, with subtly different rules and uses:
- **Static nested classes** — a class declared
staticinside another. Doesn't reach into the enclosing instance. - **Inner classes** — a non-static class inside another. Carries a hidden reference to the enclosing instance.
- **Local classes** — a class declared inside a method.
- **Anonymous classes** — a class declared and instantiated in one expression, no name given.
Plus lambdas (Java 8+), which look like anonymous classes but compile differently and have their own rules.
You'll rarely write inner or local classes in modern code — lambdas and records have taken over most of their uses. But they show up in older codebases, and a few patterns (especially static nested classes for builders) are still very much current. Worth knowing what each does.
Static nested classes
A class declared static inside another acts almost like a top-level class. It can be instantiated without an enclosing instance. It can't directly access the outer class's instance fields.
public class Outer {
private static int counter = 0;
private int instanceCount = 0;
public static class Builder {
private String name;
public Builder withName(String n) {
this.name = n;
return this;
}
public Outer build() {
counter++; // static — accessible
// instanceCount++; // would NOT compile — no instance
return new Outer();
}
}
}
Outer.Builder b = new Outer.Builder();
Outer o = b.withName("Alice").build();
**The classic use: builders.** When a class has many optional fields, instead of constructor overloads or 17 setters, expose a static nested Builder class:
public class HttpRequest {
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeout;
private HttpRequest(Builder b) {
this.url = b.url;
this.headers = Map.copyOf(b.headers);
this.body = b.body;
this.timeout = b.timeout;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String url;
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeout = 30_000;
public Builder url(String url) { this.url = url; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String body) { this.body = body; return this; }
public Builder timeout(int t) { this.timeout = t; return this; }
public HttpRequest build() {
if (url == null) throw new IllegalStateException("url required");
return new HttpRequest(this);
}
}
}
HttpRequest req = HttpRequest.builder()
.url("https://api.example.com")
.header("Accept", "application/json")
.timeout(5_000)
.build();
Fluent, explicit, immutable. The standard library uses this pattern (HttpClient.newBuilder(), Stream.Builder, StringBuilder isn't quite this but similar spirit).
**Why static nested instead of a separate top-level class?** Three reasons:
- The classes are tightly coupled — the Builder is meaningless without HttpRequest.
- The Builder can access private constructors and fields of the outer class.
- It keeps the public API namespace clean: HttpRequest.Builder, not HttpRequestBuilder floating around.
Inner classes (non-static)
Drop the static keyword and the nested class becomes an **inner class**. It carries a hidden reference to the enclosing instance — you can access the outer's instance fields directly.
public class LinkedList<E> {
private Node head;
private class Node { // inner class
E value;
Node next;
void remove() {
// can access LinkedList's instance state via the implicit outer reference
if (head == this) {
head = this.next;
}
}
}
}
To create an inner class instance from outside the enclosing class:
LinkedList<String>.Node node = list.new Node(); // unusual syntax — rarely seen
Most of the time, inner classes are private and created internally.
**Costs.** The hidden outer reference means every inner-class instance pins the enclosing instance in memory. If an inner class instance outlives its outer (passed to a long-lived collection, registered as a listener), the entire outer object can't be garbage collected. Classic memory leak.
**Modern advice.** When you find yourself reaching for an inner class, ask first: would a static nested class work? Usually yes. Static nested classes don't carry the outer reference, so they're safer and lighter. Only use a non-static inner class when you genuinely need the implicit outer-instance access — for example, when implementing an iterator that needs to walk the outer collection's nodes.
Local classes
A class declared inside a method. Visible only within that method.
public void process(List<String> input) {
class Sorter implements Comparator<String> {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
}
input.sort(new Sorter());
}
The class can access the method's local variables (provided they're final or effectively final — same rule as lambdas).
In practice, local classes are rare. They were useful before Java 8 for one-off implementations of single-method interfaces. Lambdas obliterated 95% of that use case. You'll see local classes occasionally in very specific scenarios — for example, recursive helpers that need an actual class with internal state — but they're a niche tool.
If you find yourself writing a local class with one method, that's a lambda. If it's a single-method instance you only need once, use an anonymous class. If it's truly stateful and you need it elsewhere later, extract it to a top-level or static-nested class.
Anonymous classes
An anonymous class is declared and instantiated in one expression, without a name:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked!");
}
});
The new ActionListener() { ... } syntax says: "create a new class that implements ActionListener, override its method like this, and give me an instance." There's no name for the class — the compiler generates one (you'll see Outer$1, Outer$2 in compiled bytecode and stack traces).
Anonymous classes were the standard way to write event handlers, comparators, runnables, and callbacks before Java 8. They're verbose, the syntax is awkward, and stack traces become unreadable. Almost every use of them is better as a lambda in modern code:
// Anonymous class — pre-Java 8 style
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked!");
}
});
// Lambda — Java 8+
button.addActionListener(e -> System.out.println("Clicked!"));
**Where anonymous classes still win:**
- When you need to implement an interface or extend a class with **multiple methods**. Lambdas only work for single-method interfaces.
- When you need state that's specific to that instance. (Lambdas can capture, but not redefine.)
- When you specifically need a true subclass — for example, to access protected members of a parent class.
For everything else (and that's the majority), prefer lambdas.
Lambdas vs anonymous classes — the key differences
Lambdas look like syntactic sugar for anonymous classes. They aren't, quite. The differences matter.
**1. this means different things.** Inside an anonymous class, this refers to the anonymous instance. Inside a lambda, this refers to the *enclosing* instance.
public class Outer {
private String name = "Outer";
public void demo() {
Runnable anon = new Runnable() {
@Override
public void run() {
System.out.println(this); // anonymous Runnable instance
}
};
Runnable lambda = () -> System.out.println(this); // the Outer instance
}
}
**2. Compilation is different.** An anonymous class is a full compiled class file (Outer$1.class). A lambda is compiled into a method inside the enclosing class plus an invokedynamic call. Lambdas are typically more efficient — fewer classes loaded, less memory.
**3. Lambdas only work for *functional interfaces* — interfaces with exactly one abstract method.** Anonymous classes can implement anything.
**4. Variable capture rules are the same.** Both can capture local variables, but only if they're final or effectively final:
int counter = 0;
Runnable r = () -> System.out.println(counter); // OK — effectively final
counter = 5; // makes counter no longer effectively final
Runnable r2 = () -> System.out.println(counter); // compile error
The full lambda story gets its own chapter next. For now, the rule: prefer lambdas when the interface has one method; reach for anonymous classes when it has more.
Practical use cases by category
A quick reference of when each form is the right tool in modern Java.
**Static nested class — use it for:**
- Builders for classes with many optional fields
- Helper types tightly coupled to the outer class (Iterator, Entry, Node)
- Strategy implementations grouped under their parent class
- Test fixtures bundled with the class they're testing
**Non-static inner class — use it for:**
- Iterator implementations that walk the outer's data structure
- Rare cases where you genuinely need the implicit outer reference
**Local class — use it for:**
- One-off classes used in a single method that need real state or multiple methods (lambda won't fit)
- Almost never. Lambdas or top-level extraction cover most needs.
**Anonymous class — use it for:**
- Multi-method interfaces (since lambdas can't do them)
- Quick test doubles in unit tests
- Single-use overrides of protected methods
**Lambda — use it for:**
- Everything else. Functional interface implementations are this 95% of the time.
The trend over the past decade is clear: most nested-class usage has migrated to lambdas. Modern Java code uses very few inner or local classes. But static nested classes for builders, and anonymous classes for the multi-method cases, are still very much current patterns.
⁂ Back to all modules