4

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?

3 Answers 3

8

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.

Sign up to request clarification or add additional context in comments.

2 Comments

One could argue that the "accepted idiomatic way" to prevent the problem presented here, is to make the class immutable. That way the field value is also "effectively final". Of course, one would have to be a dedicated functional programmer to make that argument. (I'm not one of them)
And implementing that in Java would have such a fundamentally disruptive effect that lambdas would be dead in the water ... in the minds of most Java programmers.
5

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. :-)

Comments

2

What I normally like to do is to minimize code parts that access fields directly, so you could wrap the part starting the thread in a function like this:

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

public Thread launchAsynchTaskWithValue(int launchValue) throws Exception
{
    Thread t = new Thread(()->doSomething(launchValue));
    t.start();
    return t;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.