Concepts / Local Classes and Variable Access

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

  • Programming

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?

java
Output
0
1
2
Notes on the code
  • 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?

  • 2, 2, 2

  • 0, 1, 2

  • 3, 3, 3

  • 0, 0, 0

Reveal answer
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

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 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.

Pattern
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).
Initialized x = 5set onceNo later changex stays 5Effectively finalReassigned x = 6value changed laterNot effectively finalno further assignmentassignment later
What happens to a local variable's value over time, and at which point does reassigning it make it no longer effectively final?

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.

Pass 1i = 0, n = 0Pass 2i = 1, n = 1Pass 3i = 2, n = 2i++ then new ni++ then new n
How does i keep changing across iterations while a variable declared inside the loop body stays fixed for each lambda or local class that captures it?

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.

java
Notes on the code
  • 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.

java
Output
0
1
2
Notes on the code
  • 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.

java
Notes on the code
  • 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 happensEffect on a captured parameter
In the enclosing method, after the parameter is receivedParameter is not effectively final, so a lambda or local class cannot use it
Inside the local class that references the parameterParameter is not effectively final, because the subroutine check includes the local class
Nowhere in the subroutine after the parameter is receivedParameter 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.

Lambda or localclass uses a nameLocal variable orparameter?Changed afterinitialization?parameters: anywhere insubroutineAllowedNot allowedGlobal variable, ruledoes not applyyesnonoyes
Given a piece of code, what sequence of checks determines whether a captured local variable or parameter is allowed?

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.

Best practice

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 like
    for (int i = 0; i < 3; i++) { suppliers.add(() -> i); }
    Why it fails

    Being 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 instead

    Copy 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 like
    int n = i; suppliers.add(() -> n);  with no final keyword written
    Why it fails

    The source says the keyword is not required when the rule is already satisfied. n is effectively final even without final.

    Do this instead

    Check 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 fails

    The source states that global variables are not subject to this restriction.

    Do this instead

    Apply 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 like

    A parameter is never reassigned in the method body, but a local class assigns to it

    Why it fails

    For parameters, the source includes assignments inside any local class that references the parameter.

    Do this instead

    Search the whole subroutine, including the local class, for assignments to the parameter.

Practice Deciding Capture Legality

MEDIUM

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.
EASY

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.