Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda capturing instance variable

Tags:

java

lambda

From reading the JLS after a frustrating debugging session I find that lambdas will capture the value of effectively-final local variables, but if you refer to an instance variable it captures a reference to the variable, which has serious implications for multi-threaded code.

For example, the following is an MCVE distilled from a much larger program:

public class LambdaCapture
{
    public static void main(String[] args) throws Exception
    {
        Launcher i1 = new Launcher();
        i1.launchAsynchTask();
    }

    public static class Launcher
    {
        private int value = 10;

        public void launchAsynchTask() throws Exception
        {
            System.out.printf("In launchAsynchTask value is %s\n",value);
            Thread t = new Thread(()->doSomething(value));
            t.start();
            value = -1;
            t.join();
        }

        public void doSomething(int value)
        {
            System.out.printf("In asynch task, value is %s\n",value);
        }
    }
}

I found the output surprising. It is

In launchAsynchTask value is 10
In asynch task, value is -1

since I initially (prior to JLS research) and intuitively expected the lambda to capture the value of the variable value instead of a reference to it.

If I have to guarantee that the current value is captured instead of a reference the obvious solution is to create a local final temporary:

        final int capture = this.value;
        Thread t = new Thread(()->doSomething(capture));

My question: Is this the accepted idiomatic way to force value capture, or is there some other more natural way to do it?

like image 602
Jim Garrison Avatar asked Oct 29 '25 07:10

Jim Garrison


2 Answers

I ... intuitively expected the lambda to capture the value of the variable value instead of a reference to it.

That (capturing the value) is what happens with local variables.

With fields, what is actually happening is that you are capturing a reference to the instance of the object that the field belongs to. In your case, it is a reference to the Launcher.this object. (The same thing happens when you declare an inner class.)

My question: Is this the accepted idiomatic way to force value capture, or is there some other more natural way to do it?

I can't think of a better way.

like image 106
Stephen C Avatar answered Oct 31 '25 19:10

Stephen C


Because you're using shorthand syntax, it's not as obvious what is going on.

When you write value to access the field, it implicitly means this.value.

The lambda expression is capturing the absolutely final "local variable" this that is implicit to all non-static methods.

The lambda expression

()->doSomething(value)

is logically equivalent to

new Lambda$1(this)

where Lambda$1 is declared like this (using arbitrary names):

private static final class Lambda$1 implements Runnable {
    private final Launcher ref;
    Lambda$1(Launcher ref) {
        this.ref = ref;
    }
    @Override
    public void run() {
        this.ref.doSomething(this.ref.value);
    }
}

As you can see, the lambda expression ()->doSomething(value) is not actually capturing value. The unqualified field access is obscuring what is actually happening.


FYI: Hiding field value behind parameter value in the doSomething() method is a bad idea. The name conflict makes the code very vulnerable to misinterpretation by programmers, and good IDEs will warn you about it (unless you disabled that warning).

Hopefully that just happened by mistake here when creating an MCVE, and you wouldn't do that in real code. :-)

like image 24
Andreas Avatar answered Oct 31 '25 20:10

Andreas