JAVA / NESTED CLASSES AND THE OBJECT CONTRACT
Local and anonymous classes for one-off behaviour
Write local and anonymous classes for behaviour used in exactly one place, and know what they capture, override, and when a lambda is enough.
What you will learn
- Declare a local class inside a method when the helper is only meaningful there
- Fuse declaration and instantiation with an anonymous class for a single-use supertype
- Capture only effectively final locals; the values are copied into the object
- Pick an anonymous class over a lambda when you need fields or two methods
Understanding Local and anonymous classes for one-off behaviour
A local class is an ordinary class declaration that happens to sit inside a block: a method body, a constructor, an initialiser. Its name is in scope only until that block ends, so no other code can even mention the type. An anonymous class takes the same idea one step further by fusing the declaration into a single new expression, so there is no source name at all and callers can only hold the instance through a supertype. Both still compile to real class files named like Main$1LengthRule and Main$1, so nothing about them is special at run time: you give up a usable name and buy the fact that the behaviour sits at the one place that cares about it.
The instance you create can outlive the call that created it, as when the local class below is returned to main, so the compiler cannot let the object read the caller's stack slots. Instead it copies every captured local into a synthetic field at construction time. That copy is the entire reason the language demands captured locals be effectively final: with a copy in the object and a reassignable original in the method, a later limit = 9 would leave the object quietly holding 5. Fields of an enclosing instance are exempt because they are reached through a reference to that object rather than copied.
Choose by counting uses and by what the behaviour needs: used twice or named in a signature, promote it to a nested or top-level class; used once inside one method but carrying state, keep a local class so the name still appears in stack traces; handed straight to a call, an anonymous class saves you inventing a name. An anonymous class pays for that brevity with real limits, namely exactly one supertype, no constructor of its own, and no way for the caller to reach anything it declares beyond that supertype. A lambda is shorter still, but only for a functional interface, and it has no fields and no this of its own, so needing state or a second abstract method sends you back to an anonymous class.
public class Main {
interface Rule {
boolean allows(String name);
}
static Rule maxLengthRule(int limit) {
// local class: named, visible only inside this method, captures limit
class LengthRule implements Rule {
private int rejected = 0;
@Override
public boolean allows(String name) {
if (name.length() > limit) {
rejected++;
return false;
}
return true;
}
@Override
public String toString() {
return "LengthRule(limit=" + limit + ", rejected=" + rejected + ")";
}
}
return new LengthRule();
}
public static void main(String[] args) {
Rule shortNames = maxLengthRule(5);
// anonymous class: declaration and the one instantiation fused together
Rule noDigits = new Rule() {
@Override
public boolean allows(String name) {
for (int i = 0; i < name.length(); i++) {
if (Character.isDigit(name.charAt(i))) {
return false;
}
}
return true;
}
};
String[] names = {"ada", "grace", "margaret", "k3vin"};
for (String name : names) {
System.out.println(name + " -> length:" + shortNames.allows(name)
+ " digits:" + noDigits.allows(name));
}
System.out.println(shortNames);
System.out.println("local simple name: '" + shortNames.getClass().getSimpleName() + "'");
System.out.println("anon simple name: '" + noDigits.getClass().getSimpleName() + "'");
System.out.println("isAnonymousClass: " + noDigits.getClass().isAnonymousClass());
}
}Local and anonymous classes trade a reusable name for locality, and pay for it by copying the effectively final locals they capture.
Worked examples
Anonymous subclass with an instance initialiser
An anonymous class extending an abstract class, passing constructor arguments to the superclass and doing its own setup without a constructor.
abstract class Greeter {
private final String language;
Greeter(String language) {
this.language = language;
}
abstract String greet(String name);
@Override
public String toString() {
return "Greeter[" + language + "]";
}
}
public class Main {
public static void main(String[] args) {
String suffix = "!";
Greeter pirate = new Greeter("pirate") {
private final String prefix;
{
prefix = "Ahoy, ";
}
@Override
String greet(String name) {
return prefix + name + suffix;
}
};
System.out.println(pirate.greet("Ada"));
System.out.println(pirate);
System.out.println("extends " + pirate.getClass().getSuperclass().getSimpleName());
}
}Example explained
Line 1new Greeter("pirate") { ... }: the argument goes to the superclass constructor, because the anonymous class has none of its own.
Line 2The bare { prefix = "Ahoy, "; } block is an instance initialiser, the only place an anonymous class can run setup code, since a constructor would need a class name.
Line 3suffix is read from the enclosing method and never reassigned, so it is effectively final and the capture compiles.
Line 4println(pirate) inherits Greeter.toString, and getSuperclass() confirms the nameless class is a genuine subclass.
One captured copy per instance
Each anonymous Runnable created in the loop keeps its own copies of the values captured on that iteration.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String[] words = {"alpha", "beta", "gamma"};
List<Runnable> tasks = new ArrayList<>();
for (String word : words) {
int length = word.length();
tasks.add(new Runnable() {
@Override
public void run() {
System.out.println(word + " has " + length + " letters");
}
});
}
// for (int i = 0; i < words.length; i++) would not work here:
// i is reassigned each round, so it is not effectively final.
System.out.println("tasks created: " + tasks.size());
for (Runnable task : tasks) {
task.run();
}
}
}Example explained
Line 1The enhanced-for variable word is a fresh variable on every iteration and is never assigned in the body, so each Runnable captures a different string.
Line 2length is a new local per iteration too, and its value is copied into the object at construction, not read later from the loop.
Line 3All three tasks run after the loop has finished, and each still prints its own values because nothing points back at the dead stack frame.
Line 4A classic index-based for loop reuses one i, which is why capturing it is rejected rather than silently giving all tasks the final value.
Two methods and state: where a lambda cannot go
An anonymous Iterable returning an anonymous Iterator, which needs both hasNext and next plus per-instance state.
import java.util.Iterator;
public class Main {
static Iterable<Integer> countdown(int from) {
return new Iterable<Integer>() {
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private int remaining = from;
@Override
public boolean hasNext() {
return remaining > 0;
}
@Override
public Integer next() {
return remaining--;
}
};
}
};
}
public static void main(String[] args) {
for (int n : countdown(3)) {
System.out.println("tick " + n);
}
Iterable<Integer> twice = countdown(2);
System.out.println("first iterator: " + twice.iterator().next());
System.out.println("second iterator: " + twice.iterator().next());
}
}Example explained
Line 1Iterator declares two abstract methods, so no lambda can implement it; anonymous classes remain the tool for multi-method types.
Line 2remaining is a field of the anonymous class initialised from the captured from, which is how the countdown carries state between calls.
Line 3Iterable is a functional interface, so its anonymous form could be a lambda, but the Iterator inside it could not.
Line 4Each iterator() call builds a new anonymous instance with its own remaining, so both calls on twice start again at 2.
Important notes
An anonymous or local class created in an instance method also captures the enclosing instance, so a listener parked in a long-lived registry keeps that whole object reachable; declare it in a static method when it needs no outer state.
An anonymous class has exactly one supertype and no constructor: arguments in the new expression go to the superclass, and any setup belongs in an instance initialiser block.
Common mistakes
Incrementing a captured local counter from inside the class, which fails with "local variables referenced from an inner class must be final or effectively final"; wrapping it in an int[1] or AtomicInteger silences the compiler but introduces shared mutable state that races as soon as the task runs on another thread.
Assuming this inside an anonymous class means the enclosing object: passing this to a callback or printing it hands out the anonymous instance, so the log line reads Main$1@... and identity comparisons against the outer object fail.
Adding an extra public method to an anonymous class and then returning the instance as its supertype: the caller has no name to cast to, so that method is permanently unreachable and the code silently loses the feature.
Try it yourself
Change, predict, then run
Write a method Runnable countdownTask(String label, int from) that returns an anonymous Runnable printing label followed by each number from `from` down to 1. Create two tasks with different arguments, run both only after both exist, and confirm each printed its own captured label and starting number.
Open the Java workspaceCheck your understanding
A static method declares a local int limit, builds an anonymous Runnable that reads it, returns the Runnable, and the caller runs it much later. Why does the compiler insist that limit is effectively final?
- The Runnable stores a copy of limit taken when it was constructed, so a reassignable original could silently disagree with that copy
- Anonymous classes cannot declare fields, so any value they use has to be a constant
- The captured variable lives on the heap and the JVM forbids two references to the same heap slot
- final is required so the JIT can inline the anonymous class into the calling method
Show answer
Capture is by value: the compiler copies limit into a synthetic field when the object is created, and by the time run() executes the method's frame is gone, so there is nothing to read from or write back to. Forbidding reassignment is how the language stops the copy and the original from drifting apart. The "no fields" option is tempting because anonymous classes really do have restrictions, such as no constructor, but they can and do declare fields; the captured copy is stored in exactly such a field.