Creating Nested Classes (Advanced)
Java supports four kinds of nested types, each with different scoping and access rules. The OCP exam tests the distinctions carefully — knowing when each kind is appropriate and what members each can access is essential.
Overview of Nested Types
| Kind | Declared where | static? |
Accesses outer instance? | Name |
|---|---|---|---|---|
| Inner class | Inside a class body | No | Yes | Has a name |
| Static nested class | Inside a class body | Yes | No | Has a name |
| Local class | Inside a method | No | Yes (if final/effectively final) |
Has a name |
| Anonymous class | Inside an expression | No | Yes (if final/effectively final) |
No name |
Inner Classes: Accessing Outer Members
An inner class holds an implicit reference to the enclosing instance. It can access all members of the outer class — including private ones:
javapublic class Outer {
private int value = 10;
class Inner {
private int value = 20;
void display() {
int value = 30;
System.out.println(value); // 30 — local variable
System.out.println(this.value); // 20 — Inner field
System.out.println(Outer.this.value); // 10 — Outer field
}
}
}
// Instantiation requires an outer instance:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.display();Memory consideration: every Inner instance holds a reference to its Outer instance, which can prevent garbage collection. Prefer static nested classes when the inner class does not need the outer instance.
Static Nested Classes: Usage Patterns
A static nested class is associated with the enclosing type, not any instance. It cannot access instance members of the outer class directly:
javapublic class LinkedList<T> {
private Node<T> head;
// Static nested class — no access to LinkedList instance fields
static class Node<T> {
T data;
Node<T> next;
Node(T data) { this.data = data; }
}
}
// Instantiated without an outer instance:
LinkedList.Node<String> node = new LinkedList.Node<>("hello");Common usage patterns for static nested classes:
- Builder pattern — the
Builderclass lives inside the class it builds - Value holder —
Map.Entry<K,V>is a static nested interface insideMap - Implementation details — data structure nodes (like
Nodeabove)
javapublic class HttpRequest {
private final String url;
private final String method;
private HttpRequest(Builder b) {
this.url = b.url;
this.method = b.method;
}
public static class Builder {
private String url;
private String method = "GET";
public Builder url(String url) { this.url = url; return this; }
public Builder method(String method) { this.method = method; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
HttpRequest req = new HttpRequest.Builder()
.url("https://example.com")
.method("POST")
.build();Local Classes in Methods
A local class is declared inside a method body. It can access local variables from the enclosing method, but only if they are final or effectively final (not reassigned after initialisation):
javapublic static void process(List<String> items) {
final String prefix = "item-"; // effectively final
class Formatter {
String format(String s) {
return prefix + s.toUpperCase(); // captured from enclosing scope
}
}
Formatter fmt = new Formatter();
items.forEach(i -> System.out.println(fmt.format(i)));
}Local classes can implement interfaces and extend classes. They are useful when a helper type is needed only inside a single method and a lambda is insufficient (e.g., when you need multiple methods or internal state).
Anonymous Classes
An anonymous class is a local class without a name — it is declared and instantiated in a single expression. It is common for one-off implementations of interfaces or abstract classes:
As Runnable
javaThread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running in thread: " + Thread.currentThread().getName());
}
});
t.start();
// Modern equivalent: new Thread(() -> System.out.println("..."))As Comparator
javaList<String> words = new ArrayList<>(List.of("banana", "apple", "cherry"));
words.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
// Modern equivalent: words.sort(Comparator.comparingInt(String::length));As a functional interface with state
When the implementation needs more logic than a single lambda expression cleanly handles:
javainterface Validator<T> {
boolean isValid(T value);
default Validator<T> and(Validator<T> other) {
return value -> this.isValid(value) && other.isValid(value);
}
}
Validator<String> notEmpty = new Validator<>() {
private int checks = 0;
@Override
public boolean isValid(String s) {
checks++;
return s != null && !s.isBlank();
}
};Capture Rules Summary
| Variable type | Accessible in inner / local / anonymous class? |
|---|---|
| Instance field of enclosing class | Yes (inner class only — requires outer reference) |
| Static field of enclosing class | Yes (all nested kinds) |
| Local variable of enclosing method | Only if final or effectively final |
| Method parameter of enclosing method | Only if final or effectively final |
Key Rules to Remember
- Inner classes require an enclosing instance; use
Outer.this.fieldto disambiguate shadowed names - Static nested classes have no link to an enclosing instance; they are preferred when the outer reference is not needed
- Local and anonymous classes capture effectively final local variables — reassignment after declaration breaks the rule
- Anonymous classes can extend a class or implement an interface — never both in the same expression
- Anonymous classes cannot have explicit constructors; use instance initializers
{}for setup - Modern Java often replaces anonymous classes with lambdas or method references, but anonymous classes remain necessary when you need internal state or multiple methods