← Fundamentals
Guided lesson 7 questions · Java only

Java lambdas and effectively final locals

What a lambda copies when it is made, what it reads later, and why javac refuses some locals.

A Java lambda copies the value of each local variable it uses when the lambda is made, so javac requires that local never to be assigned twice. When the copied value is a reference, the lambda and the method reach one object, and changes to that object are seen. A lambda never copies a field: it keeps this and reads the field when it runs.

Written for Java.

Before you begin

Why this matters

Lambdas are passed to sorting, to collections, and to many library methods, and they often run long after the line that made them. If you know what a lambda copied and what it reads later, you can predict what it does when it finally runs, and fix the effectively final error without a workaround.

What this helps you reason about

  • Say what a lambda copied when it was made, and read the copies that the stepper shows as arg$1 and arg$2.
  • Fix “local variables referenced from a lambda expression must be final or effectively final” with a copy, a field, or a copied object, and say what each choice changes.
  • Predict what this means inside a lambda and inside an anonymous class, and what object::method keeps.

In JavaScript, Python, or C#, a function value uses the variables around it and sees every later assignment to them. A Java lambda takes copies instead, and javac refuses a local that is assigned twice. Closures and captured variables showed the copy and the error. This lesson goes further: when the copy is taken, why a captured list can still change, why fields follow other rules, what this means in a lambda and in an anonymous class, and what a method reference keeps.

Already know this? The short version
  • A lambda copies each local it uses when the line that makes it runs. javac requires every such local to be effectively final, and checks the whole method: an assignment before the lambda, after it, or inside it gives “local variables referenced from a lambda expression must be final or effectively final”, marked at the use inside the lambda.
  • A copied reference leads to the same object, so a lambda can add to a captured list and sees later changes to it; to keep the contents as they were, capture List.copyOf(list).
  • Fields and static fields are never copied: a lambda that uses a field captures this and reads or writes the field each time it runs. An array of one or an AtomicInteger used as a counter compiles, but hides shared state that belongs in a field.
  • In a lambda, this is the enclosing object; in an anonymous class, this is the anonymous object, Outer.this reaches the enclosing one, and javac 17 keeps the enclosing object in this$0 even when the class never uses it.
  • current::label evaluates current once, when the method reference is made, so current need not be effectively final and later assignments do not reach it. The section “What goes wrong with lambdas in real programs?” covers counters in arrays, loop variables, fields that change before a lambda runs, and large objects kept alive.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
capture
To use a local variable from the surrounding method inside a lambda. Java copies the variable’s value into the lambda when the lambda is made.
effectively final
A local variable that is never assigned again after it is first set. A lambda can use only local variables that are final or effectively final.
field
A variable declared in a class, outside every method. A static field belongs to the class; any other field belongs to each object of the class.
this
Inside an instance method or a constructor, a reference to the object the method was called on. A static method has no this.
anonymous class
A class with no name, declared and used in one expression, such as new Supplier<String>() { public String get() { … } }. It makes one object of a new class, and inside its methods this is that object.
functional interface
An interface with exactly one abstract method, such as IntUnaryOperator with applyAsInt. A lambda or a method reference can be used wherever one is expected.
method reference
An expression such as Main::addBonus that turns an existing method into a value of a functional interface, without calling the method.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Capture

What does a lambda copy when it is made?

Closures and captured variables showed that a Java lambda keeps a copy of each local variable it uses. This lesson looks closer: when the copy is taken, what it means for a reference, and what a lambda keeps when it uses a field.

Below, note is a Supplier<String>: an interface from java.util.function whose method get takes nothing and returns a String. The type in angle brackets is the type that get returns. When the line that makes note runs, Java copies 2 and "oat milk" into the new lambda object. Calling note.get() later runs the body with those copies; the lambda never reads main’s variables again.

Because the lambda holds copies, javac requires each local it uses to be effectively final: set once, and never assigned again anywhere in the method. The check covers the whole method, so an assignment before the lambda counts as much as one after it. Method parameters follow the same rule.

Main.java
int cartons = 2;
String drink = "oat milk";
Supplier<String> note = () -> "buy " + cartons + " " + drink;
// buy 2 oat milk
System.out.println(note.get());
// cartons = 3; anywhere in this method would not compile

A lambda copies each local it uses when it is made, and javac requires those locals never to be assigned twice anywhere in the method.

Coming from Kotlin: A Kotlin lambda can assign a captured var, because the compiler moves it into a hidden object. Java does not, so the same code is a compile error.

Coming from Python: A Python closure reads the variable itself when it runs, and nonlocal lets it assign the variable. A Java lambda has neither: it reads the copy it took when it was made.

Precisely: what javac compiles

javac turns the lambda body into a private method of Main, whose parameters receive the captured values; the stepper names its frame Main.lambda in main. When the lambda is made, the JVM creates an object of a class it generates, and that object stores the captured values in fields named arg$1, arg$2, and so on. That is why the stepper shows those names. Neither the class nor the names are promised by the language. JLS 15.27.2 states the capture rule, and JLS 4.12.4 defines effectively final: a local with an initializer is effectively final when it is never on the left of an assignment and never the operand of ++ or --.

Experiment

Watch a lambda copy two locals

When the line that makes the lambda runs, Java copies the current values of cartons and drink into the new lambda object. The lambda never reads main’s variables again. javac checks the whole method: a local that is assigned twice anywhere is not effectively final, so no lambda may use it, even a lambda made after the last assignment.

Why it matters A lambda holds copies taken when it is made. To capture a value that a method computes in steps, put the final result in a new local and capture that.

Predict what prints. Then add the line cartons = 3; just below int cartons = 2;, above the lambda, and predict whether the program still compiles.

import java.util.function.Supplier;

class Main {
    public static void main(String[] args) {
        int cartons = 2;
        String drink = "oat milk";
        Supplier<String> note = () -> "buy " + cartons + " " + drink;
        System.out.println(note.get());
    }
}
What to look for

After the Supplier line, main lists note as Supplier lambda@ and a number, then {arg$1: 2, arg$2: "oat milk"}. The stepper does not know the source names of captured values: Java names them arg$1, arg$2, and so on, so arg$1 is the copy of cartons and arg$2 the copy of drink. The number after @ is the debugger’s id for an object, not an address. Step into note.get(): the frame is named Main.lambda in main, and there the copies appear under their own names, cartons 2 and drink "oat milk". The run prints buy 2 oat milk. After the edit, the program does not compile. javac reports: error: local variables referenced from a lambda expression must be final or effectively final, with the mark under cartons inside the lambda. The new assignment comes before the lambda, and it still counts.

Expected output

buy 2 oat milk

02 / Effectively final

Why can a lambda change a list but not reassign it?

A copy of a reference leads to the same object. So a lambda that captured a list can add to it, and the method sees the change. Consumer<String> is an interface whose method accept takes a String and returns nothing.

The rule stops assignments to the variable, not changes to the object. javac refuses an assignment wherever it is: before the lambda, after it, or inside its body. Each one gives the same error, and the mark is under the use of the variable inside the lambda, not under the assignment:

error: local variables referenced from a lambda expression must be final or effectively final

An assignment inside the body, such as cartons = cartons + 1, is reported once for each use of cartons in the lambda.

There are three honest fixes, and each one means something different. If the value is final by the time the lambda is made, copy it into a new local, such as final int count = cartons;, and capture count. If the lambda must keep the contents of an object as they are now, capture a copy of the object, such as List.copyOf(shopping), which Java final, primitives, and boxing introduced. If the lambda must change a value that other code reads, the value belongs in a field, as the next section shows.

Main.java
List<String> shopping = new ArrayList<>();
Consumer<String> add = item -> shopping.add(item);
// allowed: the list changes, the variable does not
add.accept("eggs");
System.out.println(shopping); // [eggs]
// shopping = new ArrayList<>(); would not compile

Effectively final stops assignments to a captured variable; the object it refers to can still change.

Precisely: the rule is on the use

JLS 15.27.2 states the rule for each use: a local used but not declared in a lambda body must be final or effectively final. So javac reports the uses inside the lambda, and the assignment by itself is legal code. An anonymous class has the same rule, with its own message: error: local variables referenced from an inner class must be final or effectively final.

Experiment

Add to a captured list, then try to replace it

The lambda copied the reference in shopping, so it reaches the same ArrayList as main. shopping.add changes that list; it does not assign the variable shopping, so the rule is met. The edited line assigns shopping itself, so shopping is no longer effectively final, and javac rejects its use in the lambda.

Why it matters A captured reference lets the lambda and the method change one object. Only an assignment to the variable breaks the rule.

Predict what prints. Then add the line shopping = new ArrayList<>(); just above the println line, and predict whether the program compiles.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

class Main {
    public static void main(String[] args) {
        List<String> shopping = new ArrayList<>();
        Consumer<String> add = item -> shopping.add(item);
        add.accept("eggs");
        add.accept("butter");
        System.out.println(shopping);
    }
}
What to look for

shopping shows java.util.ArrayList@ and an id, then []. add shows Consumer lambda@ and a number, then {arg$1: …} with the same ArrayList id: the lambda’s copy is a reference to the same list. Each add.accept(...) stops inside the lambda, with item "eggs", then "butter". After each call, shopping and the lambda’s arg$1 both show the new element. The run prints [eggs, butter]. After the edit, javac reports: error: local variables referenced from a lambda expression must be final or effectively final, with the mark under shopping inside the lambda, not under the new assignment.

Expected output

[eggs, butter]

03 / Fields and this

What does a lambda keep when it uses a field?

A field is not a local variable, so a lambda does not copy it. When a lambda in an instance method uses a field such as openings, it uses this.openings, and it captures this: the reference to the current object. this is never assigned, so the rule is met, and the lambda reads and changes the field every time it runs. A static field is not captured at all: the lambda reads it from the class each time. Runnable, from java.lang, is the interface whose method run takes nothing and returns nothing.

So the honest place for a count that a lambda updates is a field of the object that owns the count.

Two workarounds are common. int[] opened = {0} is an array of one: the lambda captures the reference and changes opened[0]. AtomicInteger, from java.util.concurrent.atomic, is an object that holds an int; its method incrementAndGet adds 1 safely even when several threads call it at once. Both compile. Both are smells: signs that the design needs a second look. The array hides changing, shared state behind a variable that looks fixed, and its count is lost when the method returns unless something returns the array too. AtomicInteger also tells every reader that threads are involved, even when they are not. When the count belongs to an object, make it a field of that object. When it is a total over a list, a for loop with a plain int is clearer.

Main.java
class Fridge {
    int openings = 0;

    Runnable doorSensor() {
        // uses this.openings, so it captures this
        return () -> openings++;
    }
}

A lambda copies this, never a field: it reads and changes each field when it runs.

Coming from C#: C# code often counts with a captured local, count++, inside a lambda. In Java, the same counter is a field, or a value returned to the caller.

Precisely: lambdas that capture nothing

A static method has no this, so a lambda there can use static fields but no instance fields. A lambda that uses no local and no instance field captures nothing, and the stepper shows it as {}. JLS 15.27.4 lets the JVM give such a lambda a new object each time or reuse one; code must not depend on either.

Experiment

Count door openings in a field

openings++ inside the lambda means this.openings++. A field is not a local variable, so the lambda does not copy it, and it need not be effectively final. The lambda copies this, a reference that is never assigned, and reads and writes the field through it each time it runs.

Why it matters A lambda that uses a field sees every later change to the field, and can change it. A count that the lambda updates and other code reads belongs in a field.

openings is a field, not a local. Predict what prints. Then add the line fridge.openings = 10; just below Runnable sensor = fridge.doorSensor(); and predict again.

class Fridge {
    int openings = 0;

    Runnable doorSensor() {
        return () -> openings++;
    }
}

class Main {
    public static void main(String[] args) {
        Fridge fridge = new Fridge();
        Runnable sensor = fridge.doorSensor();
        sensor.run();
        sensor.run();
        System.out.println(fridge.openings);
    }
}
What to look for

Inside doorSensor, the frame lists this as Fridge@ and an id, then {openings: 0}. Back in main, sensor shows Runnable lambda@ and a number, then {arg$1: Fridge@…}, with the same id as fridge: the lambda captured this, the whole object, not a copy of openings. Each sensor.run() stops inside the lambda, and fridge then shows {openings: 1}, then {openings: 2}. The run prints 2. After the edit, fridge shows {openings: 10} before the first run, and the run prints 12.

Expected output

2

04 / Anonymous classes

What does this mean in a lambda and in an anonymous class?

Before Java 8, code passed behavior with an anonymous class: a class with no name, declared and used in one expression, such as new Supplier<String>() { public String get() { … } }. It looks like a longer lambda, but it is a new class, and that changes what names mean inside it.

In a lambda, names mean what they mean in the method around it. this is the enclosing object. A lambda parameter cannot reuse the name of a local of that method: next to int n, the lambda n -> n + 1 gives error: variable n is already defined in method main(String[]).

In an anonymous class, this is the anonymous object itself. Its methods may declare names that hide outer ones, and it can have fields of its own. To reach the enclosing object, write the class name before this, as in Fridge.this. An anonymous class made in an instance method also keeps the enclosing object in a hidden field, this$0.

In the code below, getClass().getName() returns the name of an object’s class. javac names the first anonymous class inside Fridge Fridge$1.

Main.java
Supplier<String> byLambda() {
    // Fridge
    return () -> this.getClass().getName();
}

Supplier<String> byAnonymous() {
    return new Supplier<String>() {
        public String get() {
            // Fridge$1
            return this.getClass().getName();
        }
    };
}

In a lambda, this is the enclosing object; in an anonymous class, this is the anonymous object, and Fridge.this is the enclosing one.

Coming from JavaScript: An arrow function keeps the surrounding this, like a Java lambda. A function expression gets its own this, as a Java anonymous class does.

Precisely: what an anonymous class stores

An anonymous class copies each captured local into a field named val$ followed by the local’s name, such as val$shelf, and the enclosing object into this$0. The stepper shows both names as they are. javac 17 adds this$0 to every anonymous class made in an instance method, even one that never uses the enclosing object; later javac versions leave it out in that case. A lambda keeps only what its body uses. JLS 15.9.5 describes anonymous classes, and JLS 15.8.4 describes Fridge.this.

Experiment

Ask a lambda and an anonymous class what this is

A lambda is not a class of its own, so this in its body is this of the method around it, the Fridge. An anonymous class is a new class, so this in its methods is the anonymous object. The anonymous object keeps the Fridge it was made in, in this$0, and Fridge.this reads it.

Why it matters When you turn an anonymous class into a lambda, or back, check every this in its body: it changes meaning.

getClass().getName() returns the name of an object’s class. Predict the two names. Then, in the anonymous class, change this.getClass() to Fridge.this.getClass() and predict again.

import java.util.function.Supplier;

class Fridge {
    Supplier<String> byLambda() {
        return () -> this.getClass().getName();
    }

    Supplier<String> byAnonymous() {
        return new Supplier<String>() {
            public String get() {
                return this.getClass().getName();
            }
        };
    }
}

class Main {
    public static void main(String[] args) {
        Fridge fridge = new Fridge();
        Supplier<String> a = fridge.byLambda();
        Supplier<String> b = fridge.byAnonymous();
        System.out.println(a.get() + " " + b.get());
    }
}
What to look for

a shows Supplier lambda@ and a number, then {arg$1: Fridge@…}: the lambda captured the Fridge. b shows Fridge$1@ and an id, then {this$0: Fridge@…} with the same Fridge id: an object of a new class, Fridge$1, whose hidden field this$0 holds the Fridge. Inside b.get(), the frame list shows Fridge$1.get twice. javac adds a second get method, which the interface calls and which calls yours; this is the Fridge$1 object in both. The run prints Fridge Fridge$1. After the edit, the stops are the same and the run prints Fridge Fridge: Fridge.this is the object in this$0.

Expected output

Fridge Fridge$1

05 / Interfaces and ::

Which interface does a lambda become, and what does :: keep?

A lambda has no type of its own. It takes the type of a functional interface: an interface with exactly one abstract method. The code around the lambda decides which interface, and other code runs the lambda by calling that method. java.util.function has the common ones, and Runnable is in java.lang. T and R stand for the types written in the angle brackets:

  • Runnable: run() takes nothing and returns nothing.
  • Supplier<T>: get() returns a T.
  • Consumer<T>: accept(T) returns nothing.
  • Function<T, R>: apply(T) returns an R, so a Function<String, Integer> takes a String and returns an Integer.
  • Predicate<T>: test(T) returns a boolean.

A method reference turns an existing method into such a value, without calling it. String::length can be a Function<String, Integer>, and shopping::add a Consumer<String>.

A method reference that names an object, such as current::label, evaluates current once, when the method reference is made, and keeps the object it found. So current need not be effectively final, and a later assignment to current does not reach the method reference. The lambda () -> current.label() uses the local current in its body, so javac requires current to be effectively final.

Main.java
Door current = new Door("fridge");
// keeps the fridge Door
Supplier<String> byRef = current::label;
current = new Door("freezer");
System.out.println(byRef.get()); // fridge door
// () -> current.label() would not compile here

object::method keeps the object it found when it was made, even if the variable is assigned later.

Precisely: four kinds, and null

JLS 15.13 lists four kinds of method reference: a static method (Integer::sum), a method of one object (current::label), an instance method of any object of a class (String::length, where the object is the first argument), and a constructor (ArrayList::new). If the object in current::label is null, the NullPointerException is thrown when the method reference is made, not when it is called; in the runner, that exception has no message. java.util.function also has versions for primitives, such as IntSupplier and IntPredicate, which avoid boxing.

Experiment

Keep a door through a method reference

current::label evaluates current once, when the method reference is made, and keeps the Door it found. It never reads the variable current again, so Java does not require current to be effectively final. The lambda () -> current.label() uses the local current in its body, so the rule applies to it.

Why it matters A method reference on an object keeps that object. A later assignment to the variable does not reach it.

current is assigned twice, so it is not effectively final. Predict whether this compiles, and what prints. Then change current::label to () -> current.label() and predict again.

import java.util.function.Supplier;

class Door {
    String name;

    Door(String name) {
        this.name = name;
    }

    String label() {
        return name + " door";
    }
}

class Main {
    public static void main(String[] args) {
        Door current = new Door("fridge");
        Supplier<String> byRef = current::label;
        current = new Door("freezer");
        System.out.println(byRef.get());
    }
}
What to look for

byRef shows Supplier lambda@ and a number, then {arg$1: Door@…} with {name: "fridge"}: the method reference copied the reference in current when it was made. new Door("freezer") steps into the Door constructor. After the assignment, current shows a different Door id with {name: "freezer"}, while byRef’s arg$1 keeps the fridge Door. byRef.get() steps into Door.label, where this is the fridge Door. The run prints fridge door. After the edit, javac reports: error: local variables referenced from a lambda expression must be final or effectively final, with the mark under current inside the lambda.

Expected output

fridge door

06 / In production

What goes wrong with lambdas in real programs?

Each bug below follows from a rule in this lesson.

A counter in an array of one. int[] opened = {0}; with a lambda that runs opened[0]++ compiles and counts. But the count lives in an array that only this method and the lambda can reach, each call to the method starts a new count, and the variable looks fixed while its contents change. AtomicInteger hides the same thing and suggests threads. Give the count an owner: a field of the object it belongs to.

A lambda made in a classic for loop. In for (int i = 0; i < 3; i++), a lambda cannot use i, because i++ assigns it. Copy it inside the body, int shelf = i;, and capture shelf, a new local on each pass. The enhanced for loop, for (String item : items), makes a new item on each pass, so it needs no copy.

A field that changes before the lambda runs. A lambda that uses a field reads it when it runs, not when it was made, so plans and retries made from a field all see its latest value. When the lambda must keep the value from the moment it was made, copy the field into a local first.

A large object kept alive. A listener is a function that another object stores and calls when something happens. A listener lambda that uses one field keeps this, and with it everything this refers to. An anonymous class keeps this$0 even when it uses nothing. While a long-lived object stores the listener, all of that stays reachable. Copy the field you need into a local, prefer a lambda, and remove listeners you no longer need.

Main.java
// compiles, but hides a count that changes
int[] opened = {0};
Runnable sensor = () -> opened[0]++;
// does not compile: i++ assigns i
for (int i = 0; i < 3; i++) {
    jobs.add(() -> System.out.println(i));
}

Give a changing count an owner, copy a loop counter inside the body, copy a field when its current value must be kept, and capture only what the lambda needs.

Experiment

Plan two freezer settings, then run them

target is a field, so the first lambda captured this and reads this.target when it runs. By then main has set target to -24. chosen is a local, so each lambda copies its value when plan runs: -18 for Monday, -24 for Tuesday. The edited lambdas use no field, so they no longer keep the Freezer.

Why it matters A lambda that must keep a field’s value from the moment it was made needs a local copy of that value.

Predict both lines. Then, in plan, add the line int chosen = target; above plans.add, change target in the lambda to chosen, and predict again.

import java.util.ArrayList;
import java.util.List;

class Freezer {
    int target = -18;
    List<Runnable> plans = new ArrayList<>();

    void plan(String day) {
        plans.add(() -> System.out.println(day + " " + target));
    }
}

class Main {
    public static void main(String[] args) {
        Freezer freezer = new Freezer();
        freezer.plan("Mon");
        freezer.target = -24;
        freezer.plan("Tue");
        for (Runnable plan : freezer.plans) {
            plan.run();
        }
    }
}
What to look for

Each lambda in plans shows as Runnable lambda@ and a number, then {…}: the panel does not open objects nested this deep. When the for loop reaches a plan, plan shows {arg$1: Freezer@…, arg$2: "Mon"}: this and a copy of day. The Freezer inside it already shows target: -24. Each plan.run() steps into Freezer.lambda in plan. The run prints Mon -24, then Tue -24. After the edit, the plans show {arg$1: "Mon", arg$2: -18} and {arg$1: "Tue", arg$2: -24}, with no Freezer, and the run prints Mon -18, then Tue -24.

Expected output

Mon -24
Tue -24

07 / Try it

Can you tell a copy from a later read?

One lambda uses a copied local, one uses a static field, and one is a method reference on the value of that field. Decide when each one reads mode.

Main.java
import java.util.function.Supplier;

class Main {
    static String mode = "eco";

    public static void main(String[] args) {
        String saved = mode;
        Supplier<String> a = () -> saved;
        Supplier<String> b = () -> mode;
        Supplier<String> c = mode::toUpperCase;
        mode = "fast";
        System.out.println(a.get() + " " + b.get() + " " + c.get());
    }
}

The ice maker starts in eco mode and is switched to fast. What is printed?

Reveal the reasoning

eco fast ECO. a captured saved, a local that holds a reference to the String "eco". b uses the static field mode, which is never captured: the stepper shows b as {}, and b reads Main.mode when it runs, after mode = "fast". c is a method reference: Java evaluated mode once, when c was made, and c keeps that String, "eco", so it calls toUpperCase on it. A String never changes, so mode = "fast" makes mode refer to another String and leaves "eco" as it was.

Practice tickets

  1. JAVA-201 · Yesterday’s doorbell summary shows today’s visitors → Free
  2. JAVA-202 · The baby monitor screen names the wrong room after a camera switch → Sign in · Pro
  3. JAVA-203 · Kettle switch-ons go missing after its sensor reconnects → Sign in · Pro

Keep exploring

Arrays, lists, and vectors →

Primary references