☕ Java
Nested Classes
A nested class is a class declared inside another class. Java supports four kinds of nested classes, each with distinct characteristics: static nested classes (declared static inside a class), inner classes (non-static, associated with an enclosing instance), local classes (declared inside a method), and anonymous classes (unnamed, declared and instantiated at the same point). Nesting a class inside another is a design statement: the nested class logically belongs to the enclosing class and exists to serve it. This entry covers the taxonomy of nested classes, their access rules, their memory and performance implications, when each kind is appropriate, and the relationship between nested class kinds and their enclosing context.
The Four Kinds of Nested Classes
Java's four nested class kinds differ along two axes: whether the class is static, and where it is declared. Static nested classes are declared with the static modifier inside the class body. They have no reference to an enclosing instance and behave essentially like top-level classes that happen to be scoped inside another class. Non-static nested classes — inner classes — are declared without static and carry an implicit reference to an enclosing instance of the outer class.
Local classes are declared inside a method, constructor, or initialization block. They are visible only within the scope where they are declared and can access local variables of the enclosing scope, provided those variables are effectively final. Anonymous classes are a special form of local class that have no name and combine the class declaration with a single instantiation expression using new.
The decision among these kinds follows from the relationship the nested class has with its context. If the nested class needs to access instance members of the enclosing class, it must be an inner class (non-static nested class). If it is purely a helper or a structural component that stands on its own, a static nested class is cleaner. If the class is needed only within a single method, a local class keeps it scoped tightly. If only a single instance is needed and no reusability is required, an anonymous class eliminates the named declaration entirely.
All four kinds have access to private members of the enclosing class. This is a deliberate Java design decision: nested classes are implementation details of the enclosing class and should therefore have full access to its internals. This access creates the logical coupling that justifies nesting in the first place.
Java
// ── The four nested class kinds in one file ──────────────────────────
public class Outer {
private int outerField = 10;
// ── Kind 1: Static nested class ───────────────────────────────────
// No reference to Outer instance; accessed as Outer.StaticNested
public static class StaticNested {
private int x;
public StaticNested(int x) { this.x = x; }
public void display() {
System.out.println("StaticNested x=" + x);
// Cannot access outerField — no enclosing instance
}
}
// ── Kind 2: Inner class (non-static) ──────────────────────────────
// Has implicit reference to enclosing Outer instance
public class Inner {
private int y;
public Inner(int y) { this.y = y; }
public void display() {
// CAN access outerField — has enclosing Outer instance
System.out.println("Inner y=" + y +
" outerField=" + outerField);
}
}
public void demonstrateLocalAndAnonymous() {
int localVar = 42; // must be effectively final to use in local/anon
// ── Kind 3: Local class ────────────────────────────────────────
// Visible only inside this method
class LocalHelper {
void help() {
System.out.println("LocalHelper: outerField=" +
outerField + " localVar=" + localVar);
}
}
new LocalHelper().help();
// ── Kind 4: Anonymous class ────────────────────────────────────
// Declared and instantiated at the same point, no name
Runnable anon = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous: outerField=" +
outerField + " localVar=" + localVar);
}
};
anon.run();
}
}
// ── Instantiation syntax for each kind ───────────────────────────────
Outer outer = new Outer();
// Static nested — no Outer instance needed
Outer.StaticNested staticNested = new Outer.StaticNested(5);
// Inner — REQUIRES an Outer instance
Outer.Inner inner = outer.new Inner(7);
// Local and anonymous — instantiated only inside the methodAccess Rules and the Enclosing Instance
The access rules for nested classes follow directly from their type. Static nested classes can only access static members of the enclosing class — they have no enclosing instance reference. Inner classes (non-static) can access all members of the enclosing class, including private instance fields and methods, through the implicit enclosing instance reference that the compiler inserts.
The compiler's transformation of inner classes reveals exactly what is happening. When you write an inner class, the compiler generates two class files and inserts a synthetic field — typically named this$0 — in the inner class that holds a reference to the enclosing instance. Every constructor of the inner class receives the enclosing instance as a hidden parameter. This is not just a language abstraction — it has a concrete memory implication: every inner class instance holds a reference to its enclosing instance, which keeps the enclosing instance alive for as long as the inner class instance is alive.
This implicit reference is the source of one of the most common memory leaks in Java: storing an inner class instance (often a listener or callback) in a long-lived context (a static collection, a framework registry) while the enclosing instance should have been garbage collected. The inner class keeps the enclosing instance alive. The fix is to use a static nested class (which has no enclosing instance reference) for listeners and callbacks that will be stored beyond the lifecycle of the enclosing object.
Java
// ── Access rules for each nested class kind ─────────────────────────
public class BankAccount {
private final String accountId;
private double balance;
private static int totalAccounts = 0;
public BankAccount(String id, double balance) {
this.accountId = id;
this.balance = balance;
totalAccounts++;
}
// ── Static nested — accesses only static members ──────────────────
public static class AccountAudit {
public void reportTotal() {
System.out.println("Total accounts: " + totalAccounts); // OK
// System.out.println(balance); // ERROR — no enclosing instance
// System.out.println(accountId);// ERROR — no enclosing instance
}
}
// ── Inner class — accesses all members including private ──────────
public class Transaction {
private final double amount;
private final String type;
public Transaction(double amount, String type) {
this.amount = amount;
this.type = type;
}
public void apply() {
// Direct access to enclosing BankAccount's private field
BankAccount.this.balance += amount; // explicit outer ref
System.out.printf("Applied %s of %.2f to account %s%n",
type, amount, accountId); // accountId from outer
}
}
public Transaction newTransaction(double amount, String type) {
return new Transaction(amount, type); // outer 'this' implicit
}
}
// ── Memory leak pattern with inner class ──────────────────────────────
public class MemoryLeakExample {
private byte[] largeData = new byte[1024 * 1024]; // 1MB
// ── DANGER: non-static Runnable holds reference to MemoryLeakExample ──
public Runnable createTask() {
return new Runnable() { // anonymous inner class
@Override
public void run() {
System.out.println("Task running");
// Implicitly holds reference to MemoryLeakExample.this
// Even if we never use largeData here
}
};
}
// ── FIX: static nested class — no enclosing instance reference ────
private static class SafeTask implements Runnable {
@Override
public void run() {
System.out.println("Safe task running — no outer reference");
}
}
public Runnable createSafeTask() {
return new SafeTask(); // does NOT keep MemoryLeakExample alive
}
}When to Use Nested Classes
Nested classes are a design tool, not a convenience. The decision to nest a class should be driven by logical belonging: does this class exist exclusively in the service of the enclosing class, and would it make no sense outside of it? An iterator that traverses a custom collection's internal structure logically belongs inside that collection class. A builder that constructs instances of a class logically belongs inside that class. An event type that only the enclosing class fires logically belongs inside that class.
Nesting provides two concrete benefits: it increases encapsulation (a nested class can be private, invisible outside the enclosing class) and it communicates structure (the nesting relationship signals to readers that these classes are tightly coupled by design). A Builder nested inside its target class and a Node nested inside its LinkedList are immediately understandable as structural components.
The decision among the four kinds follows a simple algorithm. If the nested class needs to access instance state of the enclosing class, use an inner class. If not, prefer a static nested class — it is lighter, has no implicit outer reference, and is instantiable without an enclosing instance. If the class is needed only within one method and involves stateful logic that benefits from a named type, use a local class. If only one instance is needed and the class is small, use an anonymous class — or better, a lambda expression if the target is a functional interface.
Java
// ── Builder pattern: static nested class that builds its outer ───────
public final class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Map.copyOf(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
}
// ── Static nested Builder — no enclosing instance needed ─────────
// Callers use: new HttpRequest.Builder("GET", url).header(...).build()
public static final class Builder {
private final String method;
private final String url;
private final Map<String, String> headers = new LinkedHashMap<>();
private String body = "";
private int timeoutMs = 30_000;
public Builder(String method, String url) {
this.method = Objects.requireNonNull(method);
this.url = Objects.requireNonNull(url);
}
public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int ms) {
this.timeoutMs = ms;
return this;
}
public HttpRequest build() {
if (url.isBlank()) throw new IllegalStateException("URL required");
return new HttpRequest(this);
}
}
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
}
// ── Node: static nested class inside LinkedList ───────────────────────
public class SimpleLinkedList<T> {
// Node logically belongs to LinkedList — nested makes this clear
// static — Node does not need the LinkedList's instance state
private static class Node<T> {
T data;
Node<T> next;
Node(T data) { this.data = data; }
}
private Node<T> head;
private int size;
public void addFirst(T data) {
Node<T> newNode = new Node<>(data);
newNode.next = head;
head = newNode;
size++;
}
public int size() { return size; }
}Related Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.