I'm using AdoptOpenJDK jdk81212-b04 on Ubuntu Linux, running on Eclipse 4.13. I have a method in Swing that creates a lambda inside a lambda; both probably get called on separate threads. It looks like this (pseudocode):
private SwingAction createAction(final Data payload) {
System.out.println(System.identityHashCode(payload));
return new SwingAction(() -> {
System.out.println(System.identityHashCode(payload));
//do stuff
//show an "are you sure?" modal dialog and get a response
//show a file selection dialog
//when the dialog completes, create a worker and show a status:
showStatusDialogWithWorker(() -> new SwingWorker() {
protected String doInBackground() {
save(payload);
}
});
You can see that the lambdas are several layers deep, and that eventually the "payload" which was captured gets saved to a file, more or less.
But before considering the layers and the threads, let's go directly to the problem:
- The first time I call
createAction(), the twoSystem.out.println()methods print the exact same hash code, indicating that the captured payload inside the lambda is the same I passed tocreateAction(). - If I later call
createAction()with a different payload, the twoSystem.out.println()values printed are different! In particular, the second line printed always indicates the same value that was printed in step 1!!
I can repeat this over and over; the actual payload passed will keep getting a different identity hash code, while the second line printed (inside the lambda) will stay the same! Eventually something will click and suddenly the numbers will be the same again, but then they will diverge with the same behavior.
Is Java somehow caching the lambda, as well as the argument that is going to the lambda? But how is this possible? The payload argument is marked final, and besides, lambda captures have to be effectively final anyway!
- Is there a Java 8 bug that doesn't recognize a lambda should not be cached if the captured variable is several lambdas deep?
- Is there a Java 8 bug that is caching lambdas and lambda arguments across threads?
- Or is there something that I don't understand about lambda capture method arguments versus method local variables?
First Failed Workaround Attempt
Yesterday I thought I could prevent this behavior simply by capturing the method parameter locally on the method stack:
private SwingAction createAction(final Data payload) {
final Data theRealPayload = payload;
System.out.println(System.identityHashCode(theRealPayload));
return new SwingAction(() -> {
System.out.println(System.identityHashCode(theRealPayload));
//do stuff
//show an "are you sure?" modal dialog and get a response
//show a file selection dialog
//when the dialog completes, create a worker and show a status:
showStatusDialogWithWorker(() -> new SwingWorker() {
protected String doInBackground() {
save(theRealPayload);
}
});
With that single line Data theRealPayload = payload, if I henceforth used theRealPayload instead of payload suddenly the bug no longer appeared, and every single time I called createAction(), the two printed lines indicate exactly the same instance of the captured variable.
However today this workaround has stopped working.
Separate Bug Fix Addresses Problem; But Why?
I found a separate bug that was throwing an exception inside showStatusDialogWithWorker(). Basically showStatusDialogWithWorker() is supposed to create the worker (in the passed lambda) and show a status dialog until the worker is finished. There was a bug that would create the worker correctly, but fail to create the dialog, throwing an exception that would bubble up and never get caught. I fixed this bug so that the showStatusDialogWithWorker() successfully shows the dialog when the worker is running and then closes it after the worker finishes. I can now no longer reproduce the lambda capture issue.
But why does something inside showStatusDialogWithWorker() relate to the problem at all? When I was printing out System.identityHashCode() outside and inside the lambda, and the values were differing, this was happening before showStatusDialogWithWorker() was being called, and before the exception was being thrown. Why should a later exception make a difference?
Besides, the fundamental question remains: how is it even possible that a final parameter passed by a method and captured by a lambda could ever change?
finalmethod argument change outside and inside the lambda? And why would capturing the variable as afinallocal variable on the stack make any difference?finalvariable on the stack no longer fixes the problem. I continue to investigate.SwingActionitself. What do you do with theSwingActionreturned from the method?