Concepts / Lambda Expressions and Variable Access

Lambda Expressions and Variable Access

A local variable is effectively final if its value is never changed after it is initialized, and a parameter is effectively final if its value is not changed inside the subroutine, including inside any local class that references it

  • Programming

Why a Lambda Cannot Reach Just Any Variable

A common first reaction to lambda expressions and local classes is to assume they can reach any variable that is visible where they are written. If a variable is in scope, the reasoning goes, the lambda should be able to use it. That assumption feels natural, and for local variables and parameters it is wrong. Java places a restriction on which of those variables a lambda expression or a local class may use, and the restriction is stated in terms of a property called being effectively final.

This article builds that rule step by step. You will first trace a loop that creates several small functions, then learn the definition of effectively final, then see exactly which variables pass the test and which fail it. The restriction looks like a small syntax quirk at first, but it is really about one question: can the value the lambda sees change after the lambda has been created?

Tracing a Loop That Builds Functions

The idea is easiest to see in a loop that builds several small functions. Read the code below before reading any rule. Notice which variable is declared once per pass through the loop body and which variable is changed by the loop itself. The question to carry forward is whether a lambda created in one pass can safely refer to each of those variables.

java
Notes on the code
  • i is the loop counter; the loop update i++ changes it on every pass
  • n is declared fresh inside the loop body on every pass
  • the lambda refers to n, not to i

What do you think happens?

If the lambda inside the loop were written as suppliers.add(() -> i); instead, would this code compile?

  • Yes, because i is visible inside the loop body

  • No, because i is changed by the loop and is not effectively final

Reveal answer
Answer:

No, because i is changed by the loop and is not effectively final

Visibility is not enough. The loop update i++ changes i after it is set, so i fails the effectively final test and the lambda cannot use it directly.

What Effectively Final Means

Definition

A local variable is effectively final if its value is never changed after it is initialized. A parameter is effectively final if its value is not changed inside the subroutine, including inside any local class that references it.

The rule that matters is this: a local variable or parameter used by a local class or lambda expression must be declared final or be effectively final. The keyword final is not required when the rule is already satisfied. In other words, you do not have to write final on a variable that is never reassigned. The property is what counts, and the keyword is just one way to state it explicitly.

Tracing i and n Through Each Pass

Now return to the loop. The variable n and the variable i look similar: both hold integers and both are visible inside the loop body. The difference appears only when you ask whether their values ever change after they are set. The source states the case directly: n is effectively final and can therefore be used in the lambda expression. The variable i is not effectively final, because its value is changed when i++ runs. Using i directly inside the lambda would therefore be illegal.

i = 0n = 0i = 1n = 1i = 2n = 2i++i++new n each passnew n each pass
As the loop runs, what does each lambda capture: the shared counter i, which keeps changing, or a new variable n created on each pass that never changes?

The diagram shows why i fails the test. Its value does not stay put after initialization, because the loop keeps replacing it. The variable n has a different life. Each pass through the loop body executes the declaration int n = i again, which creates a fresh n holding the current value of i. Once that n is set, nothing in the loop body changes it, so its value never changes after initialization. That single fact is what makes it safe to capture.

Deciding Whether a Capture Is Allowed

When you meet a lambda or local class in unfamiliar code, you can work through a short sequence of questions. Start by identifying where the variable comes from. If it is a global variable, the restriction does not apply. If it is a local variable or a parameter, check whether its value changes after it is set. For a parameter, the relevant question is whether it is changed inside the subroutine, including inside any local class that references it.

Variable used inlambda or localclassLocal variable orparameter?Global variable: norestrictionCapture allowedChanged afterinitialization?Not allowed: compileerrornoyesnoyes
What sequence of checks determines whether a lambda or local class is allowed to use a given variable?

The flowchart captures the rule in the order you would apply it. A variable that is not local and not a parameter is outside this restriction. A local variable or parameter must pass the effectively final test, or be declared final, before a lambda or local class can use it. Note that the decision is about change, not about the variable's name or type.

Parameters Inside Methods and Local Classes

Parameters follow the same logic as local variables, but the change that matters is the change made inside the subroutine. A parameter that is never reassigned in the method body is effectively final, so a local class or lambda in that method may reference it. A parameter that is reassigned inside the method is not effectively final, even if the reassignment happens after the local class was written.

SituationEffectively final?Can a lambda or local class use it?
Local variable set once and never reassignedYesYes
Local variable changed by i++ in a loopNoNo, directly
Fresh variable declared inside the loop body and never changedYesYes
Parameter never reassigned inside the methodYesYes, including from a local class
Parameter reassigned inside the methodNoNo

Which variables satisfy the effectively final rule

Worked Decision: A Counter Versus a Copy

Worked example

Can the lambda use this variable?

A loop runs with a counter i from 0 to 2. Inside the body, the code declares int n = i and then creates a lambda that returns n. A second lambda tries to return i directly. Decide which lambda is legal.

  1. 1
    Identify the first variable

    n is a local variable declared inside the loop body. It is set once per pass to the current value of i.

  2. 2
    Check whether n changes after initialization

    Nothing in the loop body reassigns n after its declaration, so its value never changes after initialization. n is effectively final.

  3. 3
    Identify the second variable

    i is the loop counter. The update i++ changes it on every pass.

  4. 4
    Check whether i changes after initialization

    i is changed when i++ runs, so it is not effectively final.

  5. 5
    Apply the rule

    The lambda returning n is allowed. The lambda returning i directly is illegal because i fails the effectively final test.

Result

The lambda that uses n compiles; the lambda that uses i directly does not. The fix is to copy i into a fresh variable inside the loop body and capture that copy.

Mistakes Learners Make With Captured Variables

  • Common mistake

    Assuming that any visible variable can be used inside a lambda

    What it looks like

    Writing suppliers.add(() -> i); inside a loop where i is the loop counter.

    Why it fails

    Visibility is not the rule. Local variables and parameters must be effectively final or declared final.

    Do this instead

    Copy the value into a fresh variable declared in the loop body, then capture that variable.

  • Common mistake

    Believing the final keyword must always be written

    What it looks like

    Declaring int n = i; without final and then wondering why the lambda compiles.

    Why it fails

    The keyword is not required when the variable already satisfies the rule. The property is what matters.

    Do this instead

    Check whether the value changes after initialization. If it does not, the variable is already effectively final.

  • Common mistake

    Thinking a reassignment after the lambda is written does not matter

    What it looks like

    A local variable is captured by a lambda and then reassigned later in the same method.

    Why it fails

    The rule concerns whether the variable is changed after initialization, so a later reassignment breaks effective finality.

    Do this instead

    Remove the reassignment, or use a new variable that is set once and never changed.

  • Common mistake

    Forgetting that a parameter reassigned inside the method also fails the test, even when a local class references it

Globals, Anonymous Classes, and Syntax Versus Rule

Edge case

Global variables are not subject to this restriction. The rule applies to local variables and parameters, and it is the reason a lambda can use a local variable only when that variable is effectively final.

The source notes that the same loop can be written with an anonymous class instead of a lambda expression. Reading the two versions side by side is useful because the anonymous class makes the captured variable visible in the method body. Both versions obey the same restriction on local variables and parameters. The difference lies in syntax, not in the variable rule. If a variable fails the test in the lambda version, it fails it in the anonymous class version too.

Practice Deciding Capture Legality

MEDIUM

In the following code, a method takes a parameter label and then creates a lambda that returns label. Before the lambda is created, the method runs label = label.trim();. Decide whether the lambda is legal, and explain your decision by naming the rule that applies.

Hints
  • Ask whether label is a local variable or parameter.
  • Check whether the value of label is changed inside the subroutine after it is set.
  • Consider whether the reassignment happens before or after the lambda is written, and what the rule says about change.

Key Takeaways

  • A local variable is effectively final if its value is never changed after initialization; a parameter is effectively final if it is not changed inside the subroutine, including inside any local class that references it.
  • A local variable or parameter used by a lambda expression or local class must be declared final or be effectively final; the final keyword is not required when the rule is already satisfied.
  • A loop counter such as i fails the test because the loop update i++ changes it, while a variable declared fresh in the loop body, such as n, can be captured safely.
  • Global variables are not subject to this restriction, and lambdas and local classes follow the same rule.
  • When deciding whether a capture is allowed, check whether the variable is local or a parameter, then check whether its value changes after it is set.