3 ms·
It simplifies the implementation immensely, because the fields in those synthetic objects can be populated with copies of the variables from the stack frame. Th
by twic 4y ago
It 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.