Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 lambda mutable variable capture from method parameter?

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:

  1. The first time I call createAction(), the two System.out.println() methods print the exact same hash code, indicating that the captured payload inside the lambda is the same I passed to createAction().
  2. If I later call createAction() with a different payload, the two System.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?

like image 583
Garret Wilson Avatar asked Nov 26 '19 00:11

Garret Wilson


People also ask

How do you pass a lambda expression as a method parameter in Java?

Passing Lambda Expressions as Arguments If you pass an integer as an argument to a function, you must have an int or Integer parameter. If you are passing an instance of a class as a parameter, you must specify the class name or the object class as a parameter to hold the object.

How are variables used outside of lambda expression?

Because the local variables declared outside the lambda expression can be final or effectively final. The rule of final or effectively final is also applicable for method parameters and exception parameters. The this and super references inside a lambda expression body are the same as their enclosing scope.

Can we change the lambda expression variable data?

Yes, you can modify local variables from inside lambdas (in the way shown by the other answers), but you should not do it.

Can we use instance variable in lambda expression?

A Lambda expression can also access an instance variable. Java Developer can change the value of the instance variable even after its defined and the value will be changed inside the lambda as well.


1 Answers

how is it even possible that a final parameter passed by a method and captured by a lambda could ever change?

It is not. As you have pointed out, unless there is a bug in the JVM, this cannot happen.

This is very difficult to pin down without a minimal reproducible example. Here are the observations you have made:

  1. The first time I call createAction(), the two System.out.println() methods print the exact same hash code, indicating that the captured payload inside the lambda is the same I passed to createAction().
  2. If I later call createAction() with a different payload, the two System.out.println() values printed are different! In particular, the second line printed always indicates the same value that was printed in step 1!!

One possible explanation that fits the evidence is that the lambda being called the second time is in fact the lambda from the first run, and the lambda that was created by the second run has been discarded. That would give the above observations, and would place the bug inside the code that you have not shown here.

Perhaps you could add some extra logging to record: a) the ids of any lambdas created inside createAction at creation time (I think you would need to change the lambdas into anon classes that implement the callback interfaces with logging in their constructors) b) the ids of the lambdas at the time of their invocation

I think that the above logging would be sufficient to prove or disprove my theory.

GL!

like image 107
Rich Avatar answered Oct 19 '22 00:10

Rich