At first, we can take a look at the JLS, which states the following:
Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression must either be declared final or be effectively final (§4.12.4), or a compile-time error occurs where the use is attempted.
Any local variable used but not declared in a lambda body must be definitely assigned (§16 (Definite Assignment)) before the lambda body, or a compile-time error occurs.
Similar rules on variable use apply in the body of an inner class (§8.1.3). The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems. Compared to the final restriction, it reduces the clerical burden on programmers.
The restriction to effectively final variables includes standard loop variables, but not enhanced-for loop variables, which are treated as distinct for each iteration of the loop (§14.14.2).
To understand it better, take a look at this example class:
public class LambdaTest {
public static void main(String[] args) {
LambdaTest test = new LambdaTest();
test.returnConsumer().accept("Hello");
test.returnConsumerWithInstanceVariable().accept("Hello");
test.returnConsumerWithLocalFinalVariable().accept("Hello");
}
String string = " world!";
Consumer<String> returnConsumer() {
return ((s) -> {System.out.println(s);});
}
Consumer<String> returnConsumerWithInstanceVariable() {
return ((s) -> {System.out.println(s + string);});
}
Consumer<String> returnConsumerWithLocalFinalVariable() {
final String foo = " you there!";
return ((s) -> {System.out.println(s + foo);});
}
}
Output of main is
Hello
Hello world!
Hello you there!
This is because returning a lambda here is much the same as creating a new anonymous class with new Consumer<String>() {...}
. Your lambda - an instance of Consumer<String>
has a reference to the class it has been created in. You could rewrite the returnConsumerWithInstanceVariable()
to use System.out.println(s + LambdaTest.this.string)
, this would do exactly the same. This is why you are allowed to to access (and modify) instance variables.
If you have a (effectively) final local variable in your method, you can access it because it gets copied to your lambda instance.
But, however, if its not final, what do you think should happen in the following context:
Consumer<String> returnConsumerBad() {
String foo = " you there!";
Consumer<String> results = ((s) -> {System.out.println(s + foo);});
foo = " to all of you!";
return results;
}
Should the value get copied to your instance, but then not get updated when the local variable is updated? That would probably cause confusion, as I think many programmers would expected foo
to have the new value "to all of you" after returning this lambda.
If you had a primitive value, it would lay on the stack. So you could not simply reference the local variable, because it could disappear after the end of your method is reached.