4 ms·
I've been annoyed more than once by Java's “local variables referenced from a lambda expression must be final or effectively final” error. I used to suspect tha
by fweimer 4y ago
I've been annoyed more than once by Java's “local variables referenced from a lambda expression must be final or effectively final” error. I used to suspect that this was just an excuse to simplify the javac implementation, but I'm not so sure anymore. It is interesting to see the issue from the other side.
The issue is not restricted to variables declared in loop headers, so the proposed loop change for Go might only be the start.
- masklinn 4y ago> The issue is not restricted to variables declared in loop headers, so the proposed loop change for Go might only be the start. Technically it’s not but practically it’s by far the most common way for this to unexpectedly arise. The other cases like closing over a variable and then modifying it before the closure is invoked are a lot less common to hit unexpectedly, and a lot harder to fix nicely (short of Java’s big hammer).
- titzer 4y ago> I used to suspect that this was just an excuse to simplify the javac implementation, but I'm not so sure anymore. AFAICT it doesn't simplify javac much, if at all. It still needs to synthesize closure objects with fields to store the closed-over values. It's just that those fields can be final. I think Java did this to avoid programmer confusion. I think it was the right choice.
- twic 4y agoIt simplifies the implementation immensely, because the fields in those synthetic objects can be populated with copies of the variables from the stack frame. This: void foo() { int a = 1; Runnable r = () -> System.out.println(a); r.run(); // prints 1 } Can get turned into something like this: class r_closure implements Runnable { final int a; r_closure(int a) { this.a = a; } @Override public void run() { System.out.println(a); } } void foo() { int a = 1; Runnable r = new r_closure(a); r.run(); // prints 1 } The local a is a perfectly normal local, and the field a is a perfectly normal field. What would have to happen if the variable was mutable? For example, if you wanted to write this: void foo() { int a = 1; Runnable r = () -> System.out.println(a); a = 2; r.run(); // prints 2 } You have to transform it to something like this: class r_closure implements Runnable { int a; r_closure(int a) { this.a = a; } @Override public void run() { System.out.println(a); } } void foo() { int _a = 1; r_closure r = new r_closure(_a); r.a = 2; r.run(); } Where there is no local, and where the method looks like it's accessing a local, it's actually reaching into the closure and mutating its field! Now think about doing this if you've captured a variable in two closures, or a variable number of closures in a list. The wheels come off this approach. Instead, you would have to promote the shared mutable variable to its own object, like this: class int_box { int i; int_box(int i) { this.i = i; } } class q_closure implements Runnable { final int_box a; q_closure(int_box a) { this.a = a; } @Override public void run() { System.out.println("q = " + a.i); } } class r_closure implements Runnable { final int_box a; r_closure(int_box a) { this.a = a; } @Override public void run() { System.out.println("r = " + a.i); } } void foo() { int_box a = new int_box(1); Runnable q = new q_closure(a); Runnable r = new r_closure(a); a.i = 2; q.run(); // prints 2 r.run(); // prints 2 } Now you've taken a simple local variable which just need to be copied, and turned into its own thing on the heap!
- fweimer 4y agoIt gets even worse. In the general case, you need to use a separate object for each captured mutable variable for space safety reasons (avoiding unwanted object retention not present in the source code). The easiest way to achieve space safety involves a separate object for each captured variable, but of course that is unnecessarily wasteful in many cases. But in order to coalesce captured locals into fewer reference objects, you need to do some sort of lifetime analysis to find cases where locals become unused at the same time.