Inner Classes: Static, Anonymous and Local
Java allows classes to be defined inside other classes. These nested types come in four flavours (static nested, inner (non-static), local, and anonymous) each with different access rules and lifecycle characteristics. Choosing the right kind keeps code cohesive and avoids subtle memory leaks.
Static Nested Class
A static nested class is declared with the static
public class Graph {
private final List<Node> nodes = new ArrayList<>();
// Static nested — no Graph instance needed to create a Node
public static class Node {
private final int id;
private final String label;
private final List<Node> neighbours = new ArrayList<>();
public Node(int id, String label) {
this.id = id;
this.label = label;
}
public void addNeighbour(Node n) { neighbours.add(n); }
public List<Node> neighbours() { return Collections.unmodifiableList(neighbours); }
public int id() { return id; }
public String label() { return label; }
@Override
public String toString() { return label + "(" + id + ")"; }
}
public void addNode(Node n) { nodes.add(n); }
public List<Node> nodes() { return Collections.unmodifiableList(nodes); }
public static void main(String[] args) {
// Created without a Graph instance
Graph.Node a = new Graph.Node(1, "A");
Graph.Node b = new Graph.Node(2, "B");
Graph.Node c = new Graph.Node(3, "C");
a.addNeighbour(b);
b.addNeighbour(c);
Graph g = new Graph();
g.addNode(a);
g.addNode(b);
g.addNode(c);
System.out.println(a.neighbours()); // [B(2)]
}
}
The classic JDK example is Map.Entry<K,V>java.util.Map
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder b) {
this.method = b.method;
this.url = b.url;
this.headers = Collections.unmodifiableMap(new LinkedHashMap<>(b.headers));
this.body = b.body;
}
public String method() { return method; }
public String url() { return url; }
public String body() { return body; }
public Map<String, String> headers() { return headers; }
// Static nested Builder
public static class Builder {
private String method = "GET";
private String url;
private final Map<String, String> headers = new LinkedHashMap<>();
private String body;
public Builder url(String url) { this.url = url; return this; }
public Builder method(String method) { this.method = method; return this; }
public Builder body(String body) { this.body = body; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public HttpRequest build() {
if (url == null) throw new IllegalStateException("URL is required");
return new HttpRequest(this);
}
}
public static void main(String[] args) {
HttpRequest req = new HttpRequest.Builder()
.url("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Alice\"}")
.build();
System.out.println(req.method() + " " + req.url());
System.out.println(req.headers());
}
}
Inner (Non-Static) Class
A non-static nested class, commonly just called an inner class, holds an implicit reference to its enclosing instance. It can access all members (including private ones) of the enclosing object. To create an instance you need an enclosing instance first.
public class LinkedList<E> {
private Node<E> head;
private int size;
// Inner class — each Node implicitly has a reference to the LinkedList
// Declared private: callers only interact with the outer List API
private class Node<E> {
E data;
Node<E> next;
Node(E data) { this.data = data; }
}
public void addFirst(E element) {
Node<E> newNode = new Node<>(element); // Needs outer instance
newNode.next = head;
head = newNode;
size++;
}
public E removeFirst() {
if (head == null) throw new java.util.NoSuchElementException();
E data = head.data;
head = head.next;
size--;
return data;
}
public int size() { return size; }
}
The iterator pattern is one of the most natural uses of an inner class because the iterator needs to walk the outer collection's private state:
public class NumberRange implements Iterable<Integer> {
private final int start;
private final int end;
public NumberRange(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Iterator<Integer> iterator() {
return new RangeIterator(); // Creates inner class instance
}
// Inner class can read 'start' and 'end' from the enclosing NumberRange
private class RangeIterator implements Iterator<Integer> {
private int current = start; // Accesses enclosing field
@Override
public boolean hasNext() { return current <= end; }
@Override
public Integer next() {
if (!hasNext()) throw new java.util.NoSuchElementException();
return current++;
}
}
public static void main(String[] args) {
NumberRange range = new NumberRange(1, 5);
for (int n : range) {
System.out.print(n + " "); // 1 2 3 4 5
}
}
}
Local Class
A local class is defined inside a block of code: typically a method body. It can access effectively final local variables from the enclosing scope (same rule as lambdas). Local classes are rare in modern code but useful when an anonymous class needs a constructor or multiple methods.
public class Cipher {
public static String encrypt(String text, int shift) {
// Local class — visible only inside this method
class CaesarShifter {
private final int s;
CaesarShifter(int shift) {
this.s = ((shift % 26) + 26) % 26; // Normalise
}
char shiftChar(char c) {
if (!Character.isLetter(c)) return c;
char base = Character.isUpperCase(c) ? 'A' : 'a';
return (char) ((c - base + s) % 26 + base);
}
String apply(String input) {
StringBuilder sb = new StringBuilder(input.length());
for (char c : input.toCharArray()) sb.append(shiftChar(c));
return sb.toString();
}
}
// Can instantiate here — local class is in scope
return new CaesarShifter(shift).apply(text);
}
public static void main(String[] args) {
System.out.println(encrypt("Hello, World!", 3)); // Khoor, Zruog!
System.out.println(encrypt("Khoor, Zruog!", -3)); // Hello, World!
}
}
Like inner classes, local classes can be non-static (accessing enclosing instance members) or static-like when inside a static method. They can implement interfaces and extend classes.
Anonymous Class
An anonymous class is a class without a name, declared and instantiated in a single expression. It simultaneously defines a class and creates an instance of it. It can extend a class or implement an interface (but not both, and only one).
import java.util.*;
public class SortingDemo {
public static void main(String[] args) {
List<String> words = new ArrayList<>(
Arrays.asList("banana", "Apple", "cherry", "date"));
// Anonymous class implementing Comparator
Comparator<String> caseInsensitive = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
};
words.sort(caseInsensitive);
System.out.println(words); // [Apple, banana, cherry, date]
// Anonymous class extending an abstract class
abstract class CountingTask {
private int count = 0;
abstract void work();
void run(int times) {
for (int i = 0; i < times; i++) {
work();
count++;
}
System.out.println("Ran " + count + " times");
}
}
CountingTask printer = new CountingTask() {
@Override
void work() {
System.out.println(" Working...");
}
};
printer.run(3);
// Working...
// Working...
// Working...
// Ran 3 times
}
}
Anonymous classes can have instance initialisers and fields, which is useful when you need state that a lambda cannot have:
// Anonymous class with its own state — not possible with a stateless lambda
Runnable counter = new Runnable() {
private int count = 0;
@Override
public void run() {
System.out.println("Call #" + (++count));
}
};
counter.run(); // Call #1
counter.run(); // Call #2
counter.run(); // Call #3
When to Use Each
Here is a practical guide to choosing between the four kinds of nested types:
| Kind | Has enclosing instance? | Access enclosing members? | Has a name? | Best use case |
|---|---|---|---|---|
| Static nested | No | Static only | Yes | Builder, Entry, helper type logically tied to outer class |
| Inner (non-static) | Yes | All (including private) | Yes | Iterator, event listener that must read outer state |
| Local | Only if in instance method | Effectively-final locals, enclosing members | Yes (local only) | Complex logic inside a method needing a named helper |
| Anonymous | Only if in instance method | Effectively-final locals, enclosing members | No | Single-use implementations, especially before Java 8 |
The general rule: use static nested classes by default unless you have a concrete reason to access the enclosing instance. This avoids memory leaks and makes serialisation safer.
Note:
Lambda vs Anonymous Class
Lambdas, introduced in Java 8, replace most anonymous class usage when the target is a functional interface. But they are not identical: several differences matter in practice.
import java.util.Comparator;
import java.util.function.Predicate;
public class LambdaVsAnon {
interface Greeting {
String greet(String name);
}
public static void main(String[] args) {
// ── Comparator ──────────────────────────────────────────────────
// Anonymous class
Comparator<String> anonComp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// Lambda — identical semantics, less ceremony
Comparator<String> lambdaComp = (a, b) -> Integer.compare(a.length(), b.length());
// Method reference — even more concise
Comparator<String> mrComp = Comparator.comparingInt(String::length);
// ── 'this' refers to different things ────────────────────────────
// In an anonymous class, 'this' is the anonymous class instance
Greeting anonGreeting = new Greeting() {
@Override
public String greet(String name) {
// 'this' here is the Greeting anonymous class instance
return "Hi " + name + " from " + this.getClass().getSimpleName();
}
};
// In a lambda, 'this' is the enclosing class instance
// (LambdaVsAnon in this case)
Greeting lambdaGreeting = name -> "Hi " + name + " from lambda";
System.out.println(anonGreeting.greet("Alice"));
System.out.println(lambdaGreeting.greet("Alice"));
// ── When to still use anonymous classes ──────────────────────────
// 1. Non-functional interface (more than one abstract method)
// 2. You need the class to carry state across multiple calls
// 3. You need to extend an abstract class (lambdas can only target interfaces)
// 4. You need to reference 'this' as the implementing instance
// Example: Stateful anonymous Predicate — lambda cannot maintain 'count'
Predicate<String> countingFilter = new Predicate<String>() {
private int matchCount = 0;
@Override
public boolean test(String s) {
if (s.startsWith("J")) {
matchCount++;
System.out.println("Match #" + matchCount + ": " + s);
return true;
}
return false;
}
};
java.util.List.of("Java", "Python", "JavaScript", "Go", "Julia")
.stream()
.filter(countingFilter)
.count();
// Match #1: Java
// Match #2: JavaScript
// Match #3: Julia
}
}
Lambdas are also more efficient: they are implemented using invokedynamic.classOuter$1.class