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
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.
- 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?
Reveal 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
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.
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.
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.
| Situation | Effectively final? | Can a lambda or local class use it? |
|---|---|---|
| Local variable set once and never reassigned | Yes | Yes |
| Local variable changed by i++ in a loop | No | No, directly |
| Fresh variable declared inside the loop body and never changed | Yes | Yes |
| Parameter never reassigned inside the method | Yes | Yes, including from a local class |
| Parameter reassigned inside the method | No | No |
Which variables satisfy the effectively final rule
Worked Decision: A Counter Versus a Copy
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.
- 1Identify 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.
- 2Check 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.
- 3Identify the second variable
i is the loop counter. The update i++ changes it on every pass.
- 4Check whether i changes after initialization
i is changed when i++ runs, so it is not effectively final.
- 5Apply the rule
The lambda returning n is allowed. The lambda returning i directly is illegal because i fails the effectively final test.
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 likeWriting suppliers.add(() -> i); inside a loop where i is the loop counter.
Why it failsVisibility is not the rule. Local variables and parameters must be effectively final or declared final.
Do this insteadCopy 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 likeDeclaring int n = i; without final and then wondering why the lambda compiles.
Why it failsThe keyword is not required when the variable already satisfies the rule. The property is what matters.
Do this insteadCheck 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 likeA local variable is captured by a lambda and then reassigned later in the same method.
Why it failsThe rule concerns whether the variable is changed after initialization, so a later reassignment breaks effective finality.
Do this insteadRemove 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
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
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.