Local Classes 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 Being in Scope Is Not Enough
A common first reaction to lambda expressions and local classes is to assume that 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 property up one step at a time. You will first watch a loop that creates several small functions, then learn the definition of effectively final, then trace why one loop variable fails the test while another does not, and finally practice making the decision yourself on new code.
Tracing a Loop That Builds Functions
Read the code below before reading any rule. The loop creates three small functions, each implemented as a lambda expression, and stores them in a list. Notice which variable is declared once per pass through the loop body and which variable is changed by the loop itself. As you read, carry one question forward: can a lambda created in one pass safely refer to each of those variables?
0
1
2- The loop variable i is changed by the update i++ on every pass.
- The variable n is declared fresh inside the loop body on every pass.
- The lambda () -> n refers to n, not to i.
What do you think happens?
Before reading on: if each lambda returns the value of n from the pass that created it, which numbers do the three lambdas return when the list is read back?
Reveal answer
0, 1, 2
Each pass declares a new n holding the current value of i, and that n is never changed afterward. So the lambda created in pass 0 keeps 0, the one from pass 1 keeps 1, and the one from pass 2 keeps 2.
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 word effectively is doing real work here. The keyword final is not required. A variable can be effectively final without anyone writing final in its declaration, provided the rule is already satisfied by the code. The keyword only makes the rule explicit and asks the compiler to enforce it. A local variable or parameter used by a local class or a lambda expression must be declared final or be effectively final.
A captured local variable or parameter is allowed when it is declared final, or when no assignment to it occurs after its initialization (for a parameter, no assignment anywhere in the subroutine, including inside a local class).The diagram shows the key point. The question is not whether the variable is declared in the same place as the lambda, or whether it is visible there. The question is whether its value changes at any time after the first assignment. If a later assignment exists, the variable is not effectively final, however carefully the code is arranged.
Where i Goes Wrong and n Stays Safe
Return to the loop. The variables n and i look similar, since 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 captures 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.
- This version does not compile, because i is not effectively final.
- The fix is to copy i into a fresh variable declared inside the loop body and capture that copy instead.
Anonymous Classes Follow the Same Rule
The source notes that the same loop can be written with an anonymous class instead of a lambda expression. The anonymous class version is useful to read side by side with the lambda version because it 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.
0
1
2- The anonymous class refers to n, which is effectively final.
- Replacing n with i inside
get()would be rejected for the same reason the lambda version is.
Parameters and Local Classes
Parameters follow the same idea, with one extra place to look. A parameter is effectively final if its value is not changed inside the subroutine, and that includes inside any local class that references it. This means that a parameter can be ruled out by an assignment in the enclosing method or by an assignment inside the local class itself. The enclosing method and the local class are both part of the check.
- The parameter start is reassigned in the method body, so it is not effectively final.
- Removing the reassignment, and using a new variable such as int next = start + 1, makes the capture legal.
| Where the assignment happens | Effect on a captured parameter |
|---|---|
| In the enclosing method, after the parameter is received | Parameter is not effectively final, so a lambda or local class cannot use it |
| Inside the local class that references the parameter | Parameter is not effectively final, because the subroutine check includes the local class |
| Nowhere in the subroutine after the parameter is received | Parameter is effectively final and can be used by a lambda or local class |
How the location of an assignment changes whether a parameter can be captured
Deciding Whether a Capture Is Allowed
When you meet a lambda or local class, you can run through the same checks each time. The flowchart below turns the rule into a sequence of questions. Start from the point where a lambda or local class uses a variable, then work through the decisions in order.
Two points in the flowchart are easy to miss. First, the check for a global variable leads to a different branch, because the source states that global variables are not subject to this restriction. Second, the question asked of a local variable is about changes after initialization, while the question asked of a parameter covers the whole subroutine, including any local class that references it. Those two differences account for most of the confusion learners have when they first apply the rule.
When you need a lambda or local class to see a value that will later change, do not fight the rule. Copy the current value into a new local variable declared inside the scope where the lambda or class is created, and capture the copy. The copy is effectively final by construction, which is exactly what the loop example does with n.
Mistakes Beginners Make with Captured Variables
The following mistakes come up repeatedly when learners first apply the rule. Each one is worth recognizing, because it looks reasonable until the compiler or the rule shows otherwise.
Common mistake Assuming any variable in scope can be used by a lambda
What it looks likefor (int i = 0; i < 3; i++) { suppliers.add(() -> i); }Why it failsBeing in scope is not the test for local variables and parameters. The loop counter i is changed by i++, so it is not effectively final.
Do this insteadCopy i into a fresh variable declared in the loop body, such as int n = i, and capture n.
Common mistake Thinking the final keyword must be added before a lambda can use a variable
What it looks likeint n = i; suppliers.add(() -> n); with no final keyword writtenWhy it failsThe source says the keyword is not required when the rule is already satisfied. n is effectively final even without final.
Do this insteadCheck whether the variable is effectively final. Add final only if you want the compiler to enforce it.
Common mistake Applying the restriction to global variables
Why it failsThe source states that global variables are not subject to this restriction.
Do this insteadApply the effectively final check to local variables and parameters only.
Common mistake Checking only the enclosing method when a parameter is captured by a local class
What it looks likeA parameter is never reassigned in the method body, but a local class assigns to it
Why it failsFor parameters, the source includes assignments inside any local class that references the parameter.
Do this insteadSearch the whole subroutine, including the local class, for assignments to the parameter.
Practice Deciding Capture Legality
Consider this method. Decide whether the lambda inside the loop compiles, and explain your answer using the words changed after initialization. Then rewrite the lambda so that it compiles while keeping the loop behavior of returning 0, 2, 4 across passes.
for (int i = 0; i < 3; i++) {
int doubled = i * 2;
suppliers.add(() -> doubled);
}Hints
- Ask whether doubled is ever assigned again after its first initialization.
- Ask whether i is used directly inside the lambda. Here it is not, so the loop counter is not the problem.
- The lambda in this version refers to doubled, which is declared fresh in each pass.
A method receives a parameter named limit. Inside the method body, the line limit = limit - 1 appears before a local class is declared, and the local class returns limit. Without running the code, decide whether the local class can legally use limit, and state which part of the rule decides the answer.
Hints
- Parameters are checked across the whole subroutine, including the local class.
- A reassignment anywhere in the subroutine makes the parameter not effectively final.
Key Takeaways
- A local variable is effectively final if its value is never changed after it is initialized, and the keyword final is not required when this already holds.
- A parameter is effectively final if its value is not changed inside the subroutine, including inside any local class that references it.
- A lambda expression or local class may use a local variable or parameter only if it is declared final or is effectively final.
- The loop counter i fails because i++ changes it, while a variable such as n declared fresh in the loop body is set once per pass and can be captured safely.
- Global variables are not subject to this restriction, and anonymous classes follow the same rule as lambda expressions.