Back to Lambda Expressions
Q5Question 5 of 5

What does a lambda close over?

Key Concepts
  • Java lambdas capture the VALUE of local variables, not the variable binding itself
  • For primitives the actual value is copied; for objects the reference (memory address) is copied
  • The reference is effectively final but the object it points to can be freely mutated through its methods
  • Instance fields are accessed via the captured this reference, so fields can be read and modified
  • Java lambdas are NOT true closures because they cannot share a mutable local variable with the enclosing scope
  • The workaround for mutable counters is AtomicInteger or an instance field
  • This design is safer for concurrent programming because there are no shared mutable locals between threads

Definition

Java lambdas capture the value of local variables (not the variable binding), so the reference is final but the referenced object can be mutated.
  • Value capture – for primitives, the actual value is copied; for objects, the reference (memory address) is copied.
  • Effectively final reference – the captured reference cannot change (cannot point to a new object), but the object it points to can be mutated.
  • this capture – this is captured as a reference, so instance fields are freely readable and mutable.
  • Not a true closure – in languages like JavaScript, closures capture the variable binding, so both the closure and the enclosing scope share the same mutable variable.
  • Implication – list.add("X") works inside a lambda because the list reference is final, but count++ on a local int does not compile.

The lambda gets a photocopy of the bank form (value copy). If the original changes later, the photocopy still has the old data. But if the form contains a key to a safe-deposit box (object reference), the lambda has a copy of the key and can open the box and change what is inside. The key itself cannot change, but the box contents can.

Understanding the Concepts

What does a lambda close over?

Java lambdas capture the VALUE of local variables.

How does primitive capture work?

JAVA
int threshold = 100;
Predicate<Trade> filter = t -> t.getQuantity() > threshold;
// Lambda has a COPY of 100.
// threshold = 200;  // COMPILE ERROR: effectively final required

How does object reference capture work?

JAVA
List<String> results = new ArrayList<>();  // reference is effectively final
trades.forEach(t -> {
    results.add(t.getSymbol());  // OK: mutating the object, not the reference
});
// results = new ArrayList<>();  // COMPILE ERROR: cannot change the reference

How does this capture work?

JAVA
class TradeService {
    private int processedCount = 0;  // instance field

    void process(List<Trade> trades) {
        trades.forEach(t -> {
            processedCount++;  // OK: accessed via captured 'this'
        });
    }
}

How is this different from true closures?

JAVASCRIPT
// JavaScript: true closure captures the variable binding
let count = 0;
const increment = () => { count++; };
increment();
console.log(count);  // 1 – the SAME variable was modified
JAVA
// Java: must use AtomicInteger because local primitives cannot be captured mutably
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
Runnable increment = () -> count.incrementAndGet();
increment.run();
System.out.println(count.get());  // 1
// Reference to AtomicInteger is final. Internal value mutated via method.

What are common mistakes?

JAVA
// WRONG: trying to mutate a local primitive
int count = 0;
trades.forEach(t -> count++);  // COMPILE ERROR

// CORRECT: use AtomicInteger
AtomicInteger count = new AtomicInteger(0);
trades.forEach(t -> count.incrementAndGet());

// CORRECT: use an array (final reference, mutable contents)
int[] count = {0};
trades.forEach(t -> count[0]++);

How does capture work in a banking service?

At National Bank of India, Rajesh writes a loan processing service where a lambda captures a BigDecimal balance reference and mutates the object through its methods.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class LoanProcessor {
    private BigDecimal totalDisbursed = BigDecimal.ZERO;

    public void processLoans(List<BigDecimal> amounts) {
        AtomicInteger count = new AtomicInteger(0);
        List<BigDecimal> approved = new java.util.ArrayList<>();  // reference is final

        amounts.forEach(amt -> {
            if (amt.compareTo(new BigDecimal("50000")) <= 0) {
                approved.add(amt);           // OK: mutating list contents
                count.incrementAndGet();     // OK: mutating AtomicInteger
            }
        });

        System.out.println("Approved: " + count.get() + " loans");
        // approved = new ArrayList<>();  // COMPILE ERROR: reference is final
    }
}

The approved list reference is effectively final – the lambda cannot point it to a new list. But approved.add(amt) mutates the object the reference points to, which is allowed. The AtomicInteger is the standard workaround for a mutable counter inside a lambda.

What should a senior engineer know?

  • Mutating captured objects inside lambdas is NOT thread-safe. In parallel streams, use AtomicInteger or thread-safe collections like ConcurrentLinkedQueue.
  • The effectively-final requirement exists to prevent seeing stale copies in concurrent contexts.
  • This design is intentionally safer than true closures for Java's multi-threaded model.
  • In production banking code, prefer stream().filter().collect() over forEach-with-mutation. The functional style avoids side effects entirely, which is cleaner and thread-safe by design.

What are common interview follow-up questions?

  • Can a lambda modify an instance field? (Yes, via the captured this reference.)
  • Why can you call list.add() but not reassign list? (The reference is final, the object is mutable.)
  • Is this a true closure? (No. Java captures values, not variable bindings.)

Visual Overview

DIAGRAM
  Lambda Value Capture:

  Local int threshold = 100;
  Lambda captures: VALUE 100 (copy)
  Cannot reassign threshold. Lambda has 100 forever.

  Local List<String> list = ArrayList@0x1234;
  Lambda captures: REFERENCE 0x1234 (copy)
  Cannot change list to point elsewhere.
  CAN call list.add("X") → modifies the object at 0x1234.

  Instance field processedCount = 0;
  Lambda captures: this (reference to the object)
  CAN read/write processedCount via this.

Key Takeaway

A Java lambda captures the value of local variables, not the variable binding. For primitives, the actual value is copied. For object references, the memory address is copied. The reference cannot change (effectively final), but the object it points to can be freely mutated through its methods.

This is why list.add("X") works inside a lambda (the list reference is final, the list contents are mutable) but count++ on a local int does not compile (the value would be a stale copy). Instance fields are accessed through the captured this reference, so they can be read and modified freely.

Java lambdas are not true closures. In JavaScript, let x = 1; const f = () => x++; captures the variable binding, and calling f() modifies x in both the closure and the enclosing scope. Java cannot do this with local variables. The workaround is AtomicInteger for mutable counters or using instance fields. This design is intentionally safer for concurrent programming.

Remember one sentence: Java lambdas capture the value of locals (reference is final, object is mutable), not the variable binding like true closures.

---

Share
Use ← → arrow keys to navigate