Anonymous Inner Classes
An anonymous inner class is a class that is declared and instantiated in a single expression. It has no name and cannot be referenced by type anywhere other than the point of its creation. Anonymous classes extend a class or implement an interface and provide the body immediately in the same expression that creates the single instance. They were the idiomatic way to pass behaviour as data in Java before lambda expressions were introduced in Java 8. Understanding them remains essential because much existing code uses them, because they provide capabilities lambdas do not, and because they clarify why lambdas exist. This entry covers the syntax and mechanics of anonymous classes, their access rules, when they are still preferred over lambdas, and how the two mechanisms compare.
Anonymous Class Syntax and Mechanics
// ── Anonymous class implementing an interface ────────────────────────
Comparator<String> byLength = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
List<String> words = new ArrayList<>(
List.of("banana", "apple", "fig", "cherry"));
Collections.sort(words, byLength);
System.out.println(words); // [fig, apple, banana, cherry]
// ── Anonymous class extending a concrete class ────────────────────────
Thread t = new Thread("worker") { // extends Thread, passes name to constructor
@Override
public void run() {
System.out.println("Running in: " + getName());
}
};
t.start();
// ── Anonymous class with instance initializer and fields ─────────────
// (Cannot have a named constructor — use instance block instead)
Runnable withState = new Runnable() {
private final String config;
private int runCount = 0;
// Instance initializer replaces constructor
{
config = System.getProperty("app.mode", "default");
System.out.println("Anonymous Runnable created, mode=" + config);
}
@Override
public void run() {
runCount++;
System.out.println("Run #" + runCount + " mode=" + config);
}
};
withState.run(); // Run #1 mode=default
withState.run(); // Run #2 mode=default
// ── Anonymous class as method argument ───────────────────────────────
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked: " + e.getActionCommand());
}
});
// ── Accessing outer class members and local variables ─────────────────
public class OuterDemo {
private String outerField = "outer";
public Runnable makeTask(String taskName) { // effectively final
return new Runnable() {
@Override
public void run() {
// Access outer field (implicit OuterDemo.this reference)
System.out.println(outerField + " - " + taskName);
}
};
}
}Anonymous Classes vs Lambda Expressions
// ── Same behaviour: anonymous class vs lambda ────────────────────────
// Before Java 8 — anonymous class
Comparator<String> byLengthAnon = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// Java 8+ — lambda (same semantics, 90% less code)
Comparator<String> byLengthLambda =
(a, b) -> Integer.compare(a.length(), b.length());
// Even more concise with method reference
Comparator<String> byLengthRef = Comparator.comparingInt(String::length);
// ── 'this' semantics — the critical difference ────────────────────────
public class ThisDemo {
private String name = "OuterClass";
public void demonstrate() {
// ── Anonymous class: 'this' refers to the anonymous instance ──
Runnable anon = new Runnable() {
private String name = "AnonymousClass";
@Override
public void run() {
System.out.println(this.name); // "AnonymousClass"
System.out.println(ThisDemo.this.name); // "OuterClass"
}
};
anon.run();
// ── Lambda: 'this' refers to the enclosing ThisDemo instance ──
Runnable lambda = () -> {
System.out.println(this.name); // "OuterClass" — lambda's 'this'
// Cannot write ThisDemo.this.name — same thing, no ambiguity
};
lambda.run();
}
}
// ── When to use anonymous class (cannot replace with lambda) ──────────
// Case 1: Interface with multiple abstract methods (not functional)
MouseAdapter complexHandler = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) { /* handle click */ }
@Override
public void mouseEntered(MouseEvent e) { /* handle enter */ }
@Override
public void mouseExited(MouseEvent e) { /* handle exit */ }
// MouseAdapter provides empty defaults for all MouseListener methods
};
// Case 2: Need instance fields to maintain state
Iterator<Integer> statefulIterator = new Iterator<>() {
private int current = 0;
private int step = 2;
@Override
public boolean hasNext() { return current < 100; }
@Override
public Integer next() {
int val = current;
current += step;
return val;
}
};
// Case 3: Extending a concrete class
Thread priorityTask = new Thread("priority-task") {
@Override
public void run() { System.out.println("High priority: " + getName()); }
// Cannot do this with a lambda — Thread is a class, not an interface
};Practical Patterns with Anonymous Classes
// ── Pattern 1: Adapting a multi-method abstract class ─────────────────
// WindowAdapter provides empty implementations of all WindowListener methods
// Anonymous class overrides only the ones needed
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// Prompt to save changes before closing
if (hasUnsavedChanges()) {
int choice = JOptionPane.showConfirmDialog(
frame, "Save changes before closing?");
if (choice == JOptionPane.YES_OPTION) saveChanges();
}
System.exit(0);
}
@Override
public void windowIconified(WindowEvent e) {
System.out.println("Window minimized — pausing animations");
}
// All other methods inherited as empty stubs from WindowAdapter
});
// ── Pattern 2: Stateful behaviour in anonymous class ─────────────────
// A throttled event handler that ignores events within 500ms of the last
long minIntervalMs = 500;
ActionListener throttled = new ActionListener() {
private long lastFired = 0;
@Override
public void actionPerformed(ActionEvent e) {
long now = System.currentTimeMillis();
if (now - lastFired >= minIntervalMs) {
lastFired = now;
processEvent(e); // only fires if enough time has passed
}
}
};
// ── Pattern 3: Test stub with anonymous class ─────────────────────────
// Quick stub without a mocking framework
@Test
void processOrder_chargesPayment() {
List<PaymentRequest> charged = new ArrayList<>();
// Anonymous class stub captures what was charged
PaymentProcessor stub = new PaymentProcessor() {
@Override
public PaymentResult charge(PaymentRequest request) {
charged.add(request);
return PaymentResult.success("tx-001");
}
@Override
public PaymentResult refund(String txId, BigDecimal amount) {
return PaymentResult.success("refund-001");
}
@Override
public PaymentStatus getStatus(String txId) {
return PaymentStatus.COMPLETED;
}
};
OrderService service = new OrderService(stub);
service.processOrder(buildOrder());
assertThat(charged).hasSize(1);
assertThat(charged.get(0).getAmount())
.isEqualByComparingTo("99.99");
}
// ── Anti-pattern: double-brace initialization — avoid ─────────────────
// AVOID — creates anonymous subclass, prevents equals(), leaks outer ref
Map<String, Integer> bad = new HashMap<String, Integer>() {{
put("a", 1);
put("b", 2);
}};
// PREFER — factory method, no anonymous class, correct equals()
Map<String, Integer> good = new HashMap<>(Map.of("a", 1, "b", 2));