1. Introduction
Java 8 gives us lambdas, and by association, the notion of effectively final variables. Ever wondered why local variables captured in lambdas have to be final or effectively final?
Well, the JLS gives us a bit of a hint when it says βThe restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.β But, what does it mean?
In the next sections, weβll dig deeper into this restriction and see why Java introduced it. Weβll show examples to demonstrate how it affects single-threaded and concurrent applications, and weβll also debunk a common anti-pattern for working around this restriction.
2. Capturing Lambdas
Lambda expressions can use variables defined in an outer scope. We refer to these lambdas as capturing lambdas. They can capture static variables, instance variables, and local variables, but only local variables must be final or effectively final.
In earlier Java versions, we ran into this when an anonymous inner class captured a variable local to the method that surrounded it β we needed to add the final keyword before the local variable for the compiler to be happy.
As a bit of syntactic sugar, now the compiler can recognize situations where, while the final keyword isnβt present, the reference isnβt changing at all, meaning itβs effectively final. We could say that a variable is effectively final if the compiler wouldnβt complain were we to declare it final.
3. Local Variables in Capturing Lambdas
Simply put, this wonβt compile:
Supplier<Integer> incrementer(int start) {
return () -> start++;
}
start is a local variable, and we are trying to modify it inside of a lambda expression.
The basic reason this wonβt compile is that the lambda is capturing the value of start, meaning making a copy of it. Forcing the variable to be final avoids giving the impression that incrementing start inside the lambda could actually modify the start method parameter.
But, why does it make a copy? Well, notice that we are returning the lambda from our method. Thus, the lambda wonβt get run until after the start method parameter gets garbage collected. Java has to make a copy of start in order for this lambda to live outside of this method.
3.1. Concurrency Issues
For fun, letβs imagine for a moment that Java did allow local variables to somehow remain connected to their captured values.
What should we do here:
public void localVariableMultithreading() {
boolean run = true;
executor.execute(() -> {
while (run) {
// do operation
}
});
run = false;
}
While this looks innocent, it has the insidious problem of βvisibilityβ. Recall that each thread gets its own stack, and so how do we ensure that our while loop sees the change to the run variable in the other stack? The answer in other contexts could be using synchronized blocks or the volatile keyword.
However, because Java imposes the effectively final restriction, we donβt have to worry about complexities like this.
4. Static or Instance Variables in Capturing Lambdas
The examples before can raise some questions if we compare them with the use of static or instance variables in a lambda expression.
We can make our first example compile just by converting our start variable into an instance variable:
private int start = 0;
Supplier<Integer> incrementer() {
return () -> start++;
}
But, why can we change the value of start here?
Simply put, itβs about where member variables are stored. Local variables are on the stack, but member variables are on the heap. Because weβre dealing with heap memory, the compiler can guarantee that the lambda will have access to the latest value of start.
We can fix our second example by doing the same:
private volatile boolean run = true;
public void instanceVariableMultithreading() {
executor.execute(() -> {
while (run) {
// do operation
}
});
run = false;
}
The run variable is now visible to the lambda even when itβs executed in another thread since we added the volatile keyword.
Generally speaking, when capturing an instance variable, we could think of it as capturing the final variable this. Anyway, the fact that the compiler doesnβt complain doesnβt mean that we shouldnβt take precautions, especially in multithreading environments.
5. Avoid Workarounds
In order to get around the restriction on local variables, someone may think of using variable holders to modify the value of a local variable.
Letβs see an example that uses an array to store a variable in a single-threaded application:
public int workaroundSingleThread() {
int[] holder = new int[] { 2 };
IntStream sums = IntStream
.of(1, 2, 3)
.map(val -> val + holder[0]);
holder[0] = 0;
return sums.sum();
}
We could think that the stream is summing 2 to each value, but itβs actually summing 0 since this is the latest value available when the lambda is executed.
Letβs go one step further and execute the sum in another thread:
public void workaroundMultithreading() {
int[] holder = new int[] { 2 };
Runnable runnable = () -> System.out.println(IntStream
.of(1, 2, 3)
.map(val -> val + holder[0])
.sum());
new Thread(runnable).start();
// simulating some processing
try {
Thread.sleep(new Random().nextInt(3) * 1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
holder[0] = 0;
}
What value are we summing here? It depends on how long our simulated processing takes. If itβs short enough to let the execution of the method terminate before the other thread is executed itβll print 6, otherwise, itβll print 12.
In general, these kinds of workarounds are error-prone and can produce unpredictable results, so we should always avoid them.
6. Conclusion
In this article, weβve explained why lambda expressions can only use final or effectively final local variables. As weβve seen, this restriction comes from the different nature of these variables and how Java stores them in memory. Weβve also shown the dangers of using a common workaround.
