Lambda Expression and Variable Capture

2019-02-13 02:54发布

问题:

Please explain to me how a lambda expression can use and modify instance variables of its enclosing class, but can only use local variables of its enclosing scope. (Unless it is final or effective final?)

My basic question is how instance variables of a class are modifiable from within a lambda and local variables are not, in the context of scope.

回答1:

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 (effective) final local variable in your method, you can access it because 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.



回答2:

You can refer this article - https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood explained on lambda expressions compilation. As explained lambda expressions/blocks of code are compiled into the anonymous class, these anonymous class are compiled with the name format (<<Enclosing Class name>>$<<1(Number)>>), so suppose assume if non final local variables are allowed then compiler cannot trace it from where this local variable is referred as anonymous class' '.class' files are created/compiled with aforementioned format separately like normal java classes.

So if local variable is final then compiler creates a final instance in the anoymous class which doesn't create ambiguity to compiler. Refer the link mention above for more information