Back to Anonymous Classes
Q4Question 4 of 6

Can an anonymous class access effectively-final local variables, and why the restriction?

Key Concepts
  • An anonymous class can access local variables only if they are `final` or effectively final (assigned once and never reassigned).
  • The compiler copies the local variable's value into a hidden field of the anonymous instance (capture by copy).
  • The copy is necessary because the anonymous object can outlive the method's stack frame.
  • Reassignment after capture would create two inconsistent copies, so Java forbids it.
  • Instance fields and static fields are not subject to this restriction because they live on the heap and are shared by reference.
  • The workaround for mutable state is to capture a final reference to a mutable holder like `AtomicInteger` or a one-element array.
  • The same effectively-final rule applies identically to lambdas.

Definition

An anonymous class captures a copy of local variables, so those locals must be effectively final to keep the copy and the original consistent.
  • Effectively final – a local variable is effectively final if it is assigned exactly once and never reassigned; from Java 8 the compiler infers this without requiring the final keyword.
  • Capture by copy – the compiler copies the local variable's value into a hidden field of the anonymous instance, because the instance can outlive the method's stack frame.
  • Consistency guarantee – forbidding reassignment prevents two independent storage locations (stack local and captured copy) from diverging.
  • Instance fields exempt – fields of the enclosing class live on the heap and are shared by reference, not copied, so they are not subject to this rule.
  • Mutable-holder workaround – capture a final reference to a mutable object such as AtomicInteger, AtomicReference, or a one-element array to mutate state safely.

Imagine Rajesh fills a transfer form at National Bank of India and hands a photocopy of the amount to a temporary clerk who will process it later, possibly after Rajesh has left the branch. If Rajesh were allowed to change the amount on his original form afterwards, the clerk's photocopy and Rajesh's form would disagree. So the bank's rule is: once you hand the amount to the clerk, you cannot change it. The amount is effectively frozen.

Understanding the Concepts

What does effectively final mean?

A local variable is effectively final if it is assigned exactly once and never reassigned afterward. Before Java 8 you had to write final explicitly. From Java 8 the compiler infers it, so you may omit final as long as you never reassign the variable.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class EffectivelyFinalDemo {
    public void process() {
        BigDecimal fee = new BigDecimal("250.00"); // effectively final
        // fee = new BigDecimal("300.00"); // uncommenting makes it NOT effectively final

        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("Processing fee: " + fee + " INR");
            }
        };
        task.run();
    }

    public static void main(String[] args) {
        new EffectivelyFinalDemo().process();
    }
}
TEXT
Processing fee: 250.00 INR

How does capture by copy work internally?

When an anonymous class refers to a local variable, the compiler copies that value into a hidden field of the anonymous instance. This is necessary because the anonymous object can outlive the method. Once the method returns, its stack frame is destroyed, but the anonymous object may still be referenced and executed later.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class CaptureDemo {
    public Runnable createTask() {
        BigDecimal transferAmount = new BigDecimal("100000"); // on stack
        // The compiler copies transferAmount into the anonymous class's hidden field
        return new Runnable() {
            @Override
            public void run() {
                // Uses the captured copy, not the stack local
                System.out.println("Deferred transfer: " + transferAmount + " INR");
            }
        };
        // After this method returns, the stack local is gone
        // but the anonymous object still has its copy
    }

    public static void main(String[] args) {
        Runnable task = new CaptureDemo().createTask();
        task.run(); // works because the copy survives
    }
}
TEXT
Deferred transfer: 100000 INR

Why does Java require effectively final?

Because the anonymous object holds a copy, allowing reassignment of the local would create two independent storage locations for the same variable: the stack local and the captured copy. Changing one would not change the other, so the programmer could not reason about which value the anonymous class sees. Java chose to forbid reassignment rather than silently maintain two diverging values.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class WhyEffectivelyFinal {
    public void brokenExample() {
        BigDecimal rate = new BigDecimal("0.08");

        Runnable task = new Runnable() {
            @Override
            public void run() {
                // System.out.println("Rate: " + rate); // would use the OLD value
            }
        };

        // rate = new BigDecimal("0.09"); // COMPILE ERROR:
        // "local variables referenced from an inner class must be final or effectively final"
    }
}

What happens when you violate the rule?

The compiler rejects the code with a clear error message. Both the reassignment and the capture site are flagged.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class ViolationDemo {
    public void processTransfers() {
        BigDecimal total = BigDecimal.ZERO;

        Runnable task = new Runnable() {
            @Override
            public void run() {
                // COMPILE ERROR: total is not effectively final
                // System.out.println("Total: " + total);
            }
        };

        total = new BigDecimal("50000"); // this reassignment breaks effectively-final
    }
}

What is the mutable-holder workaround?

Capture an effectively-final reference to a mutable object. The reference itself never changes, so it stays effectively final, but the object's contents can be updated. Both the anonymous class and the calling code share the same heap object.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

public class MutableHolderDemo {
    public void batchProcess() {
        // The reference 'total' is effectively final
        // But its contents can be mutated
        AtomicReference<BigDecimal> total = new AtomicReference<>(BigDecimal.ZERO);

        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                total.updateAndGet(t -> t.add(new BigDecimal("75000")));
            }
        };

        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                total.updateAndGet(t -> t.add(new BigDecimal("125000")));
            }
        };

        task1.run();
        task2.run();
        System.out.println("Batch total: " + total.get() + " INR");
    }

    public static void main(String[] args) {
        new MutableHolderDemo().batchProcess();
    }
}
TEXT
Batch total: 200000 INR

How is this different from instance fields?

Instance fields and static fields are not subject to the effectively-final restriction because they live on the heap and are accessed through a reference to the enclosing object, not copied. The anonymous class accesses them via Outer.this, which is a shared reference, not a snapshot.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class FieldVsLocal {
    private BigDecimal balance = new BigDecimal("500000"); // instance field

    public void updateBalance() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                // Instance field: no effectively-final restriction
                balance = balance.add(new BigDecimal("10000"));
                System.out.println("Updated balance: " + balance + " INR");
            }
        };
        task.run();
    }

    public static void main(String[] args) {
        new FieldVsLocal().updateBalance();
    }
}
TEXT
Updated balance: 510000 INR

What about thread safety when using mutable holders?

Capture by copy gives you a stable snapshot that is inherently safe to pass to another thread. The mutable-holder workaround opts out of that safety, so you must handle thread-safety yourself. Use AtomicInteger or AtomicReference instead of a plain array for multi-threaded banking jobs.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadSafeMutableHolder {
    public void countSuccessfulTransfers() throws InterruptedException {
        AtomicInteger successCount = new AtomicInteger(0);

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // Simulate Priya's transfer succeeding
                successCount.incrementAndGet();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // Simulate Rajesh's transfer succeeding
                successCount.incrementAndGet();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Successful transfers: " + successCount.get());
    }

    public static void main(String[] args) throws InterruptedException {
        new ThreadSafeMutableHolder().countSuccessfulTransfers();
    }
}
TEXT
Successful transfers: 2

Does the same rule apply to lambdas?

Yes. Both anonymous classes and lambdas capture local variables in the same way (by copy), so the effectively-final rule applies identically to both. The mechanism is the same; only the syntax differs.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class LambdaSameRule {
    public void demo() {
        BigDecimal rate = new BigDecimal("0.05"); // effectively final

        // Lambda captures the same way as anonymous class
        Runnable lambda = () -> System.out.println("Interest rate: " + rate);
        lambda.run();

        // rate = new BigDecimal("0.06"); // COMPILE ERROR for lambda too
    }

    public static void main(String[] args) {
        new LambdaSameRule().demo();
    }
}
TEXT
Interest rate: 0.05

Visual Overview

DIAGRAM
┌─────────────────────────────────────────────┐
│    Effectively Final: Capture by Copy       │
├─────────────────────────────────────────────┤
│                                             │
│  Method Stack Frame                         │
│  ┌───────────────────┐                      │
│  │ BigDecimal fee    │──── value: 250.00    │
│  └────────┬──────────┘                      │
│           │ copy at creation time            │
│           ▼                                 │
│  Anonymous Instance (Heap)                  │
│  ┌───────────────────┐                      │
│  │ hidden$fee        │──── value: 250.00    │
│  └───────────────────┘                      │
│                                             │
│  Method returns → stack frame destroyed     │
│  Anonymous object survives with its copy    │
│                                             │
├─────────────────────────────────────────────┤
│  Mutable Holder Workaround                  │
│                                             │
│  AtomicReference<BigDecimal> total          │
│  ┌───────────────────┐                      │
│  │ reference (final) │──→ heap object       │
│  └───────────────────┘    ┌──────────┐      │
│                           │ value:   │      │
│  Anonymous class ─────────┤ mutable  │      │
│  (shares same ref)        └──────────┘      │
│                                             │
└─────────────────────────────────────────────┘

Key Takeaway

An anonymous class can access local variables of the enclosing method, but only if those variables are final or effectively final, meaning they are assigned exactly once and never reassigned. The same rule applies identically to lambdas, because both capture local variables in the same way: by copy.

The reason for the restriction is capture by copy. When an anonymous class refers to a local variable, the compiler copies that value into a hidden field of the anonymous instance. This is necessary because the anonymous object can outlive the method that created it. Once the method returns, its stack frame is destroyed, but the object carries its own copy. If Java allowed reassignment of the local, the stack copy and the captured copy would diverge, and nobody could reason about which value the anonymous class sees.

In a banking project at National Bank of India, Rajesh builds anonymous callbacks for batch transfer processing. Each transfer's reference and amount are effectively final, so each callback safely captures its own stable snapshot. The capture-by-copy behaviour is actually a benefit here, because the snapshot is safe to run on another thread without a data race.

When you genuinely need mutable shared state, use the standard workaround of a final reference to a mutable holder such as AtomicReference. The reference itself never changes, so it stays effectively final, but the value inside it can be updated, and every callback sees the update because they share the same heap object.

Remember one sentence: an anonymous class captures a copy of local variables, so those variables must be effectively final to prevent two diverging copies, and the clean answer to needing mutation is a shared atomic holder.

---

Share
Use ← → arrow keys to navigate