Closures
A closure is a function bundled together with the variables from its enclosing lexical scope that it refers to, captured at the time the function is created, so that the function can keep using those variables even after the scope that originally held them has finished executing. In Java, both lambda expressions and anonymous inner classes form closures: they can refer to local variables and parameters of the method that creates them, and to the fields of the enclosing object, even after that method has returned and its stack frame is gone. Java places one significant restriction on this that languages like JavaScript do not: any local variable or parameter captured by a lambda or inner class must be effectively final — assigned exactly once and never reassigned — which rules out the kind of closure-over-a-mutable-counter pattern that is idiomatic in other languages, and requires a small workaround whenever genuinely mutable shared state is needed. This entry covers what a closure captures and how, why Java enforces the effectively-final rule, the standard workarounds for needing mutable captured state, and the practical patterns and memory implications of using closures in real code.
What a Closure Is, and How a Java Lambda Captures Its Environment
// ── Capturing a local variable: a VALUE snapshot, taken at creation time ─
static Runnable makeGreeter(String name) {
// name is a parameter of this method — a local variable from the
// lambda's point of view
return () -> System.out.println("Hello, " + name + "!");
// The lambda captures the VALUE of name at this moment; once
// makeGreeter returns, its stack frame is gone, but the lambda still
// has its own private copy of "the value name had."
}
Runnable greetEarth = makeGreeter("Earth");
Runnable greetMars = makeGreeter("Mars");
greetEarth.run(); // "Hello, Earth!"
greetMars.run(); // "Hello, Mars!" — each closure has its OWN captured value
// ── Capturing an instance field: a REFERENCE to the enclosing object ───
class Counter {
private int count = 0;
Runnable incrementer() {
// This lambda captures the enclosing Counter instance implicitly,
// not a snapshot of count — it reads the field through the
// instance reference every time:
return () -> { count++; System.out.println("count = " + count); };
}
}
Counter c = new Counter();
Runnable inc = c.incrementer();
inc.run(); // count = 1
inc.run(); // count = 2 — the closure sees the field's LIVE, current value,
// unlike the value-snapshot behavior for local variables above.Effectively Final: Why Java Restricts Closures Over Local Variables
// ── Compile error: capturing a variable that is later reassigned ───────
void brokenCounter() {
int count = 0;
Runnable increment = () -> count++; // COMPILE ERROR:
// "local variables referenced from a lambda expression must be
// final or effectively final" — count is reassigned elsewhere
// in this method (right below), so it's disqualified.
count = 5;
}
// ── Workaround #1: a single-element array as a mutable holder ──────────
Runnable makeArrayCounter() {
int[] count = new int[]{0}; // the array REFERENCE is effectively
// final — never reassigned; only its
// CONTENTS change.
return () -> {
count[0]++;
System.out.println("count = " + count[0]);
};
}
// ── Workaround #2: AtomicInteger — also thread-safe ─────────────────────
Runnable makeAtomicCounter() {
AtomicInteger count = new AtomicInteger(0); // reference never reassigned
return () -> System.out.println("count = " + count.incrementAndGet());
}
Runnable counter = makeAtomicCounter();
counter.run(); // count = 1
counter.run(); // count = 2
// ── Workaround #3: a small custom mutable holder ────────────────────────
class IntHolder {
int value;
IntHolder(int v) { value = v; }
}
Runnable makeHolderCounter() {
IntHolder holder = new IntHolder(0);
return () -> System.out.println("count = " + (++holder.value));
}Closures in Practice: Factories, Callbacks, and Lifetime
// ── Factory + closures: each call produces an independent closure ──────
static Function<Integer, Integer> makeAdder(int amount) {
return x -> x + amount; // captures the amount by value, fresh each call
}
Function<Integer, Integer> addFive = makeAdder(5);
Function<Integer, Integer> addTen = makeAdder(10);
System.out.println(addFive.apply(1)); // 6
System.out.println(addTen.apply(1)); // 11 — completely independent of addFive
// ── Callback registration: a closure carrying context to be used later ─
class Button {
private final List<Runnable> clickHandlers = new ArrayList<>();
void onClick(Runnable handler) { clickHandlers.add(handler); }
void simulateClick() { clickHandlers.forEach(Runnable::run); }
}
void wireUpButton(Button button, String username) {
// This closure captures username — context the Button itself
// knows nothing about — and carries it forward to be used on click:
button.onClick(() -> System.out.println(username + " clicked the button"));
}
// ── A memory-leak trap: a lambda unnecessarily capturing the enclosing instance
class ReportGenerator {
private final byte[] hugeBuffer = new byte[100_000_000]; // large owned resource
private final String reportTitle = "Q4 Report";
// LEAK-PRONE: this instance lambda implicitly captures the enclosing
// instance, even though it only needs reportTitle — keeping hugeBuffer
// reachable for as long as the returned Runnable is reachable:
Runnable titlePrinterLeaky() {
return () -> System.out.println(reportTitle);
}
// FIXED: copy just the needed field into a local variable first;
// the lambda then captures only that String value, not the instance:
Runnable titlePrinterFixed() {
String title = this.reportTitle;
return () -> System.out.println(title);
}
}
// If titlePrinterLeaky()'s Runnable is stored in some long-lived registry
// (e.g. a static List<Runnable>), the entire ReportGenerator — including
// its 100 MB buffer — cannot be garbage-collected while that Runnable lives,
// even though the Runnable's logic never touches the buffer at all.