Nested Classes
A nested class is a class declared inside another class. Java allows this so that a type that exists only to serve one other class can live inside it: logically grouped, optionally hidden from the rest of the program, and with access to the enclosing class's private members. Nested classes are common in real code: builders, iterators, map entries, event handlers, and small data types are often nested.
The Kinds of Nested Classes
Nested classes
├── Static nested class static class Inner { } (no link to an outer object)
└── Inner classes (non-static)
├── Member inner class class Inner { } (tied to an outer object)
├── Local class declared inside a method
└── Anonymous class new Interface() { ... } (no name, one expression)
| Kind | Declared | Needs an outer instance? | Typical use |
|---|---|---|---|
| Static nested class | As a class member, with static | No | Builders, helper types, small data types |
| Member inner class | As a class member, without static | Yes | Iterators and views over the outer object's data |
| Local class | Inside a method body | Yes, if in an instance method | A helper needed only within one method |
| Anonymous class | Inline, in an expression | Yes, if in an instance method | One-off implementations of interfaces |
Nested interfaces, enums, and records are always implicitly static.
This lesson covers static nested classes and the general rules. Inner classes and anonymous classes have their own lessons.
Static Nested Classes
public class Order {
private final String id;
private final List<Line> lines = new ArrayList<>();
public Order(String id) { this.id = id; }
public void add(String product, int qty, double price) {
lines.add(new Line(product, qty, price));
}
public double total() {
double sum = 0;
for (Line l : lines) sum += l.subtotal();
return sum;
}
public static class Line {
private final String product;
private final int qty;
private final double price;
public Line(String product, int qty, double price) {
this.product = product;
this.qty = qty;
this.price = price;
}
public double subtotal() { return qty * price; }
}
}
Order order = new Order("ORD-1001");
order.add("Pen", 10, 12.5);
order.add("Notebook", 2, 80);
System.out.println(order.total()); // 285.0
Order.Line line = new Order.Line("Bag", 1, 999); // created without any Order
A static nested class behaves like an ordinary top-level class that happens to be declared inside another:
- Its full name is
Outer.Nested(hereOrder.Line). - It is created with
new Outer.Nested(...), with no outer object required. - It has no reference to an instance of the outer class, so it cannot use the outer class's instance fields directly.
- It can access the outer class's
private staticmembers, and the private instance members of any outer object it is given.
Compiled, it becomes a separate file: Order$Line.class.
Why nest it at all?
- Grouping.
Order.Linesays clearly that a line belongs to an order. A top-level class namedLinewould be ambiguous. - Encapsulation. A
private staticnested class is invisible outside the enclosing class, keeping helper types out of the package's API:
public class Cache {
private final Map<String, Entry> entries = new HashMap<>();
private static class Entry { // implementation detail
final String value;
final long expiresAt;
Entry(String value, long expiresAt) {
this.value = value;
this.expiresAt = expiresAt;
}
}
public void put(String key, String value, long ttlMillis) {
entries.put(key, new Entry(value, System.currentTimeMillis() + ttlMillis));
}
public String get(String key) {
Entry e = entries.get(key);
if (e == null || e.expiresAt < System.currentTimeMillis()) {
return null;
}
return e.value;
}
}
Callers see only put and get. Entry can be redesigned without affecting anyone.
- Shared private access. The outer and nested classes can access each other's private members, which suits tightly coupled pairs such as a class and its builder.
Access Between Outer and Nested Classes
A nested class is a member of its enclosing class, so the usual privacy boundary does not separate them:
public class Account {
private double balance; // private to Account
private static int created = 0;
static class Auditor {
void check(Account a) {
System.out.println(a.balance); // allowed: nested class reads outer's private field
System.out.println(created); // allowed: private static field of the outer class
}
}
void inspect(Auditor auditor) {
auditor.check(this);
}
}
The reverse also holds: the outer class can access private members of its nested classes. This mutual access is intentional, since both are parts of one unit of code.
The Builder Pattern with a Static Nested Class
The most common use of a static nested class in modern Java is a builder:
public class HttpConfig {
private final String baseUrl;
private final int timeoutSeconds;
private final int retries;
private final boolean followRedirects;
private HttpConfig(Builder b) { // only the builder can construct
this.baseUrl = b.baseUrl;
this.timeoutSeconds = b.timeoutSeconds;
this.retries = b.retries;
this.followRedirects = b.followRedirects;
}
public static Builder builder(String baseUrl) {
return new Builder(baseUrl);
}
public static class Builder {
private final String baseUrl;
private int timeoutSeconds = 30; // defaults
private int retries = 0;
private boolean followRedirects = true;
private Builder(String baseUrl) { this.baseUrl = baseUrl; }
public Builder timeout(int seconds) { this.timeoutSeconds = seconds; return this; }
public Builder retries(int n) { this.retries = n; return this; }
public Builder noRedirects() { this.followRedirects = false; return this; }
public HttpConfig build() {
if (timeoutSeconds <= 0) throw new IllegalStateException("timeout must be positive");
return new HttpConfig(this);
}
}
@Override
public String toString() {
return baseUrl + " timeout=" + timeoutSeconds + "s retries=" + retries
+ " redirects=" + followRedirects;
}
}
HttpConfig cfg = HttpConfig.builder("https://api.example.com")
.timeout(10)
.retries(3)
.build();
System.out.println(cfg);
// https://api.example.com timeout=10s retries=3 redirects=true
HttpConfig's constructor is private, yet Builder can call it, because they are nested in the same class. Likewise, the constructor reads the builder's private fields directly. The builder must be static: it is created before any HttpConfig exists, so it cannot depend on an outer instance.
Static Versus Inner: The Key Difference
A non-static nested (inner) class carries a hidden reference to the outer object that created it:
public class Playlist {
private final List<String> songs = new ArrayList<>();
class Cursor { // inner: tied to one Playlist
private int position = 0;
String next() {
return songs.get(position++ % songs.size()); // reads the outer object's songs
}
}
}
Playlist p = new Playlist();
Playlist.Cursor c = p.new Cursor(); // requires an outer instance
| Static nested | Inner (non-static) | |
|---|---|---|
| Created with | new Outer.Nested() | outerObject.new Inner() |
| Accesses outer instance fields | Only through an explicit reference | Directly |
| Holds a reference to the outer object | No | Yes (hidden) |
| Memory | Independent | Keeps the outer object alive while it exists |
The hidden reference has a cost: an inner object prevents its outer object from being garbage collected, which can cause memory leaks when the inner object is long-lived (stored in a cache, registered as a listener, or handed to another thread).
Rule of thumb: declare nested classes static unless they genuinely need access to an outer instance. Inner classes are covered in the next lesson.
Nested Records, Enums, and Interfaces
These are implicitly static, and they are a convenient way to define small types close to where they are used:
public class Weather {
public enum Condition { SUNNY, CLOUDY, RAIN, STORM }
public record Reading(String city, double tempC, Condition condition) { }
public interface Alert {
boolean triggers(Reading r);
}
}
var r = new Weather.Reading("Chennai", 34.5, Weather.Condition.SUNNY);
Weather.Alert heat = reading -> reading.tempC() > 33;
System.out.println(heat.triggers(r)); // true
Accessing Nested Types from Outside
A public nested type is referred to through its outer class, or imported directly:
import com.example.shop.Order;
Order.Line line = new Order.Line("Pen", 1, 10);
// or
import com.example.shop.Order.Line;
Line line = new Line("Pen", 1, 10);
The JDK uses this convention widely: Map.Entry, Character.UnicodeBlock, Thread.State, AbstractMap.SimpleEntry.
Practical Example: Leaderboard
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Leaderboard {
private final String game;
private final List<Entry> entries = new ArrayList<>();
public Leaderboard(String game) { this.game = game; }
public void submit(String player, int score) {
for (Entry e : entries) {
if (e.player.equals(player)) {
e.best = Math.max(e.best, score); // outer class reads/writes nested private fields
return;
}
}
entries.add(new Entry(player, score));
}
public List<String> top(int n) {
List<Entry> sorted = new ArrayList<>(entries);
sorted.sort(Entry.BY_SCORE_DESC);
List<String> result = new ArrayList<>();
for (int i = 0; i < Math.min(n, sorted.size()); i++) {
result.add((i + 1) + ". " + sorted.get(i));
}
return result;
}
private static class Entry {
static final Comparator<Entry> BY_SCORE_DESC =
Comparator.comparingInt((Entry e) -> e.best).reversed()
.thenComparing(e -> e.player);
private final String player;
private int best;
Entry(String player, int best) {
this.player = player;
this.best = best;
}
@Override
public String toString() { return player + " (" + best + ")"; }
}
public static void main(String[] args) {
Leaderboard board = new Leaderboard("Typing Race");
board.submit("ada", 82);
board.submit("linus", 95);
board.submit("grace", 88);
board.submit("ada", 97); // improves ada's best
board.submit("linus", 90); // lower than best; ignored
System.out.println(board.game);
board.top(3).forEach(System.out::println);
}
}
Output:
Typing Race
1. ada (97)
2. linus (95)
3. grace (88)
Entry is a private static nested class: an implementation detail of Leaderboard that the rest of the program never sees, so its fields can be accessed directly by the outer class without getters. It is static because an entry does not need a reference back to the leaderboard. The comparator lives inside Entry, next to the fields it compares. If the leaderboard later switches to a Map or a database, only code inside this one file changes.