I think I've met this classic situation in JavaScript.
Usually the programmer would expect this code below to print "Peter", "Paul", "Mary".
But it doesn't. Could anyone explain exactly why it works this way in Java?
This Java 8 code compiles OK and prints 3 times "Mary".
I guess it's a matter of how it's implemented deep down
but ... doesn't this indicate a wrong underlying implementation?
import java.util.List;
import java.util.ArrayList;
public class Test008 {
public static void main(String[] args) {
String[] names = { "Peter", "Paul", "Mary" };
List<Runnable> runners = new ArrayList<>();
int[] ind = {0};
for (int i = 0; i < names.length; i++){
ind[0] = i;
runners.add(() -> System.out.println(names[ind[0]]));
}
for (int k=0; k<runners.size(); k++){
runners.get(k).run();
}
}
}
On the contrary, if I use an enhanced for loop (while adding the Runnables), the correct (i.e. all the different) values are captured.
for (String name : names){
runners.add(() -> System.out.println(name));
}
Finally, if I use a classic for loop (while adding the Runnables), then I get a compilation error (which makes perfect sense, as the variable i
is not final or effectively final).
for (int i = 0; i < names.length; i++){
runners.add(() -> System.out.println(names[i]));
}
EDIT:
My point is: why is not the value of names[ind[0]]
captured (the value it has at the moment I add the Runnables)? It should not matter when I execute the lambda expressions, right? I mean, OK, in the version with the enhanced for loop, I also execute the Runnables later but the correct/distinct values were captured earlier (when adding the Runnables).
In other words, why does not Java always have this by value / snapshot semantics (if I may put it this way) when capturing values? Wouldn't it be cleaner and make more sense?
In other words, why does not Java always have this by value / snapshot semantics (if I may put it this way) when capturing values? Wouldn't it be cleaner and make more sense?
Java lambdas do have capture-by-value semantics.
When you do:
runners.add(() -> System.out.println(names[ind[0]]));
the lambda here is capturing two values: ind
and names
. Both of these values happen to be object references, but an object reference is a value just like '3'. When a captured object reference refers to a mutable object, this is where things can get confusing, because their state during lambda capture and their state during lambda invocation may be different. Specifically, the array to which ind
refers does change in this way, which is the cause of your problem.
What the lambda does not capture is the value of the expression ind[0]
. Instead, it captures the reference ind
, and, when the lambda is invoked, performs the dereference ind[0]
. Lambdas close over values from their lexically enclosing scope; ind
is a value in the lexically enclosing scope, but ind[0]
is not. We capture ind
and use it further at evaluation time.
You are somehow expecting here that the lambda will do a full snapshot of all objects in the entire heap that are reachable through the captured references, but that's not how it works -- nor would that make a lot of sense.
Summary: Lambdas capture all captured arguments -- including object references -- by value. But an object reference and the object to which it refers are not the same thing. If you capture a reference to a mutable object, the object's state may have changed by the time the lambda is invoked.
I would say the code does exactly what you tell him to do.
You create "Runnable"'s which print the name at the position from ind[0]
.
But that expression gets evaluated in your second for-loop. And at this point ind[0]=2
. So the expression prints "Mary"
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With