Java
inner classes
local variables
final variables
effectively final

local variables referenced from an inner class must be final or effectively final

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The Java error "local variables referenced from an inner class must be final or effectively final" appears when an anonymous class, local class, or lambda captures a method-local variable that later changes. The rule exists because Java captures the variable's value, not a live stack slot that can keep mutating after the method returns.

What "Captured" Means

When an inner class uses a local variable from the enclosing method, Java copies that value into the generated object. Consider this example:

java
1public class Demo {
2    public void printMessage() {
3        String message = "hello";
4
5        Runnable task = new Runnable() {
6            @Override
7            public void run() {
8                System.out.println(message);
9            }
10        };
11
12        task.run();
13    }
14}

This compiles because message is never reassigned. Java treats it as effectively final.

Now compare that with a failing version:

java
1public class Demo {
2    public void printMessage() {
3        String message = "hello";
4
5        Runnable task = () -> System.out.println(message);
6        message = "updated";
7
8        task.run();
9    }
10}

This fails because the lambda captures message, and then the method tries to change it. If Java allowed that, the language would need more complicated semantics for variables that live past the method call and may be shared across threads.

Final Versus Effectively Final

A variable is final if you declare it with the final keyword. A variable is effectively final if you do not reassign it after initialization.

These two examples behave the same:

java
final int timeout = 30;
Runnable a = () -> System.out.println(timeout);
java
int timeout = 30;
Runnable b = () -> System.out.println(timeout);

The second version compiles because timeout never changes. Since Java 8, explicitly writing final is often unnecessary when the variable is already effectively final.

The important limitation is reassignment. Even a small change such as timeout++ breaks effective finality.

Why Java Enforces This Rule

Method-local variables normally live on the stack. An inner class instance may outlive the method call that created it. That means the runtime cannot safely keep a reference to the original local variable in the usual way.

Java solves this by capturing a stable value. Requiring final or effectively final variables guarantees that the captured value does not drift away from what the programmer sees in the source code.

This design also avoids confusing concurrency behavior. If a background thread runs a lambda later, a captured immutable value is much easier to reason about than a mutable local that no longer exists in the original stack frame.

How to Fix the Error

The best fix depends on what you actually want.

If the value should never change, simply stop reassigning it:

java
1public void send() {
2    String host = "api.example.com";
3    Runnable task = () -> System.out.println(host);
4    task.run();
5}

If you need mutable state, move that state to an object field:

java
1public class Counter {
2    private int count = 0;
3
4    public Runnable buildTask() {
5        return () -> {
6            count++;
7            System.out.println(count);
8        };
9    }
10}

If you only need a mutable holder inside a method, use a wrapper such as AtomicInteger:

java
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class Demo {
4    public void runTask() {
5        AtomicInteger count = new AtomicInteger(0);
6
7        Runnable task = () -> {
8            int next = count.incrementAndGet();
9            System.out.println(next);
10        };
11
12        task.run();
13        task.run();
14    }
15}

This compiles because the reference to count never changes, even though the object it points to is mutable.

Lambdas and Inner Classes Follow the Same Rule

Many developers first meet this error with lambdas, but the rule is not specific to lambdas. Anonymous classes, local classes, and lambdas all capture local variables using the same basic principle.

That means the following also fails:

java
1public void example() {
2    int limit = 10;
3
4    class Checker {
5        boolean ok(int value) {
6            return value < limit;
7        }
8    }
9
10    limit = 20;
11}

The local class Checker captures limit, so reassigning limit is illegal.

Common Pitfalls

One common mistake is assuming "I only change it after the lambda runs" should be allowed. Java checks the code structure, not your intended runtime order.

Another mistake is using a one-element array just to dodge the compiler. That works technically, but it often hides design problems. If the state is truly shared and mutable, an instance field or dedicated holder class is usually clearer.

Developers also confuse object mutation with variable reassignment. Reassigning a captured reference is illegal, but mutating the object behind an unchanged reference can be legal. Whether it is a good idea is a separate design question.

Summary

  • Inner classes and lambdas capture local values from the enclosing method.
  • Captured locals must be final or effectively final.
  • Reassignment breaks effective finality, even if it happens later in the method.
  • Use a field or mutable holder when shared mutable state is really required.
  • The rule makes closure behavior simpler, safer, and easier to reason about.

Course illustration
Course illustration

All Rights Reserved.