☕ Java

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

A closure is, informally, a function with a memory: it remembers the variables that were in scope at the place it was defined, and it can still use them later, even if it is invoked somewhere else entirely, or invoked long after the code that defined it has finished running. The term describes the combination of the function itself together with this captured environment, not the function in isolation — the same lambda body created twice, in two different scopes with different local variable values, produces two closures that behave differently, because each one closes over its own environment. Java lambda expressions, and before Java 8 anonymous inner classes, form closures over two distinct things, captured in two distinct ways. A local variable or parameter from the enclosing method is captured by value: at the moment the lambda is created, the compiler copies the variable's current value into storage that belongs to the lambda object itself, so the lambda carries its own private snapshot, completely disconnected from the original variable's storage on the method's stack frame, which may well be gone by the time the lambda is actually invoked. An instance field of the enclosing object, by contrast, is captured by reference to the enclosing instance itself: the lambda keeps an implicit reference to the outer object, and reads the field through that reference every time it runs, which means it always observes the field's current value, including any mutation that happens after the lambda was created — a meaningfully different behavior from the value-snapshot used for local variables.
Java
// ── 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

Java requires that any local variable or parameter referenced inside a lambda expression or anonymous inner class be effectively final: it must be assigned exactly once along every path that reaches the lambda, and never reassigned afterward, whether or not the variable is explicitly declared with the final keyword. Attempting to capture a local variable and later reassign it anywhere in the enclosing method is a compile-time error, even if the reassignment happens after the lambda — the compiler considers the entire method body when deciding whether a variable qualifies. This restriction exists because of where local variables live and how long they last. A local variable's storage belongs to the stack frame of the method call that declared it, and that stack frame is popped — its storage reclaimed — as soon as the method returns, regardless of whether some lambda created inside that method is still alive and might be invoked later. To make this safe without keeping the entire stack frame alive indefinitely, Java's compiler copies the captured variable's value into the lambda object's own storage at the point the lambda is created, rather than sharing the original stack slot. If Java additionally allowed the lambda to write back to that original variable, the two would no longer be in sync — the lambda's mutation could not reliably affect the already-popped original storage, and if multiple lambdas captured the same variable, each would need its own independently mutable copy, raising the question of which copy's mutation is the "true" one, especially once multiple threads are involved. The effectively-final rule sidesteps the entire question by guaranteeing there is only ever one value to capture — it can never go out of sync with anything, because nothing else is allowed to change it after the lambda exists. This is a deliberate design difference from JavaScript, where a closure can capture and mutate an outer variable directly, because a JavaScript closure shares the actual variable binding rather than copying a value — that binding-sharing model is exactly what Java avoids, in favor of a simpler and, in the presence of multiple threads, safer model. When genuinely mutable shared state is needed inside a closure, Java's standard workaround is to capture a reference to a mutable container object instead of a primitive value directly: the variable holding the reference is still effectively final, since it's never reassigned and only the object it points to is mutated, while the contents of that object can change freely. A single-element array, an AtomicInteger or AtomicReference, or a small custom mutable holder class are the idiomatic choices, with the Atomic classes additionally providing thread-safety when the closure might be invoked from multiple threads.
Java
// ── 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

The factory-function pattern — a method that takes configuration data and returns a closure tailored to it — is the most common everyday use of closures, and it relies directly on the value-capture behavior described above: each call to the factory produces a fresh closure with its own independent snapshot of whatever it captured, so multiple closures created from the same factory never interfere with each other. Callback registration follows the same shape: an event listener, a completion handler passed to an asynchronous API, or a comparator built with some runtime-determined criterion are all closures created once and invoked later, often by code that has no other knowledge of the context the closure needs — the closure carries that context along with it, which is exactly the point of capturing it in the first place. Because a closure keeps its captured environment reachable for as long as the closure object itself is reachable, closures have a real memory-lifetime cost that's easy to overlook. A lambda that captures nothing, or captures only static members, holds no reference to anything beyond itself and can be freely reused or garbage-collected like any other short-lived object — in fact the JVM is permitted to, and often does, reuse a single instance for such a lambda across multiple calls, since there's no captured state to make any two invocations different. A lambda created inside an instance method, however, implicitly captures a reference to the enclosing object, even if the lambda body only actually needs one field from that instance — which means the entire enclosing object, and anything it in turn references, stays reachable and un-collectible for as long as the lambda is reachable. This becomes a genuine memory leak when such a lambda is stored somewhere long-lived, such as a static collection of registered listeners or a cache, because the lambda's hidden reference to its enclosing instance keeps that instance, and its whole object graph, alive far longer than the program logic intended, often invisibly, since nothing about the lambda's source code mentions the enclosing instance explicitly.
Java
// ── 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.

Related Topics in Functional Programming

Functional Programming Basics
Functional programming is a programming paradigm that treats computation as the evaluation and composition of functions, favors expressions that produce values over statements that mutate state, and prefers passing behavior around as data rather than encoding it only inside class hierarchies. Java was designed from the outset as an object-oriented, imperative language: methods could be invoked but never treated as values — they could not be stored in a variable, passed as an argument, or returned from another method, except through the verbose workaround of wrapping a single method inside an anonymous class that implemented a one-method interface. Java 8 closed this gap by introducing lambda expressions, method references, and a standard library of single-method functional interfaces in java.util.function, together with the Stream API built on top of them, giving Java a genuinely functional layer on top of its existing object-oriented core without abandoning that core. This entry covers what distinguishes functional style from imperative and object-oriented style, how Java represents a function as an instance of a functional-interface type rather than as a true first-class value, the standard functional interfaces the JDK provides for common shapes of behavior, and how adopting functional style changes the shape of everyday Java code.
Pure Functions
A pure function is a function whose return value depends solely on the arguments passed to it, with no dependence on any hidden or external state, and which produces no observable effect on the world beyond computing and returning that value — no mutation of arguments, no writes to shared state, no I/O, and no dependence on anything that can change between calls such as the system clock or a random number generator. Calling a pure function with the same arguments always produces the same result, a property known as referential transparency, which means a call to a pure function can be replaced anywhere in a program by its result without changing what the program does. Java does not enforce purity at the language level — any method, lambda, or method reference may freely read and write shared mutable state, perform I/O, or behave non-deterministically — so purity in Java is a discipline the developer chooses to follow rather than a guarantee the compiler provides. This entry covers the precise definition of purity and referential transparency, the common ways Java code becomes impure even unintentionally, and the practical payoffs of writing pure functions: simpler testing, safe caching and memoization, and safe parallel execution.
Higher Order Functions
A higher-order function is a function that takes one or more other functions as parameters, returns a function as its result, or both — as opposed to a first-order function, which only takes and returns plain data values like numbers, strings, or objects. Java has supported a narrow form of this since its very first versions, since passing a Runnable to a Thread or a Comparator to Collections.sort is technically passing a function (wrapped in a single-method interface) into another method, but writing an anonymous class at every call site made the pattern feel like a special case reserved for a handful of built-in APIs rather than a general technique available for everyday code. Lambda expressions and method references, introduced in Java 8, made supplying a function as an argument concise enough that higher-order functions became a natural, everyday tool — most visibly through the Stream API's map, filter, and reduce, but equally usable in ordinary application code through java.util.function. This entry covers the definition of a higher-order function, how Java represents functions as arguments and return values without true first-class functions, the most common shapes of functions that take functions as parameters, and the complementary shape of functions that build and return other functions, including function composition.
Currying
Currying is the technique of transforming a function that accepts multiple arguments into a chain of functions that each accept exactly one argument, so that calling the first function with the first argument returns a new function waiting for the second argument, and so on until every argument has been supplied and a final result is produced — a two-argument function becomes a one-argument function that, given the first argument, returns another one-argument function that, given the second, produces the result. This is distinct from partial application, which means fixing some subset of a function's arguments to produce a new function over the rest, since partial application does not require restructuring a function into a one-argument-at-a-time chain and can be done directly on an ordinary multi-argument function. Languages such as Haskell curry every function by default, with no special syntax needed to call a curried function one argument at a time; Java has no built-in support for currying, so a curried function must be constructed by hand as a chain of nested functional interfaces, most often Function<A, Function<B, R>> for two arguments, nested one layer deeper for each additional argument. This entry covers the precise definition of currying and how it differs from partial application, how to build curried functions in Java using nested Function types and a reusable curry helper, and the practical situations where currying earns its verbosity in Java versus situations where a simpler approach reads better.