A static nested class is declared with the static keyword inside the enclosing class. It has no implicit reference to an instance of the enclosing class. Think of it as a regular top-level class that is logically grouped with its enclosing type.

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> — a static nested interface inside java.util.Map. Another common use is the Builder pattern:

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()); } }

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 } } } Non-static inner classes hold a strong reference to their enclosing instance. If the inner class escapes (e.g., is passed to another thread or stored in a long-lived collection), it can prevent the outer object from being garbage collected — a classic memory leak. When in doubt, use a static nested 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.

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

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.

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 at the bytecode level and do not necessarily create a new class on every call. Anonymous classes always define a separate .class file (e.g., Outer$1.class).

Lambdas cannot shadow variables from the enclosing scope, while anonymous and local classes can (by declaring a field with the same name). If you need that kind of shadowing, a local or anonymous class is your only option.