9

I understand why the following is not accepted by the compiler:

class Foo {
    public Supplier<String> makeSupplier() {
        String str = "hello";
        Supplier<String> supp = () -> return str;

        // gives the expected compile error because
        // str is not effectively final
        // (str is a local variable, compile-time error
        //  as per JLS 15.27.2.)
        str = "world";

        return supp;
    }
}

What puzzles me is that the compiler accepts the following, and that the unit test passes:

class Bar {
    private String str = "hello";

    public void setStr(String str) {
        this.str = str;
    }

    public Supplier<String> makeSupplier() {
        Supplier<String> supp = () -> { return str; };
        return supp;
    }

    @Test 
    public void Unit_lambdaCapture() {    
        Supplier<String> supp = makeSupplier();
        Assert.assertEquals(supp.get(), "hello");
        setStr("foo");
        Assert.assertEquals(supp.get(), "foo");
    }
}

Why is the above valid and working correctly? Pointers to relevant sections of the JLS are welcome (section 15.27.2. only talks about local variables).

Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
felixx
  • 111
  • 1
  • 7
  • 6
    In your second example you return a new lambda instance with a variable captured from the context, which doesn't change in the process of creating the lambda; `str` is "effectively final", which is why it is accepted. Had you tried and reassign `str` after you had initialized the supplier, you would have a compile error as well. – fge Mar 13 '15 at 10:28
  • As @fge stated in the second example `setStr("foo")` is method-local. – Edward J Beckett Mar 13 '15 at 10:29
  • 1
    @fge Seems like a candidate for an answer, rather than a comment? – Duncan Jones Mar 13 '15 at 10:30
  • 1
    @PatrickCollins Not a duplicate, but certainly a relevant link to post. – Duncan Jones Mar 13 '15 at 10:31
  • @Duncan not really, I don't point to the correct sections of the JLS etc and I unfortunately lack the time to dig around ;) – fge Mar 13 '15 at 10:33
  • 1
    I believe that when lambda refers to the instance field, lambda actually captures `this`, not `this.str`. `this` remains unchanged and `this.str` is just an expression which is evaluated when lambda is evaluated. – vbezhenar Mar 13 '15 at 10:38
  • 1
    Possibly this answers your question: http://stackoverflow.com/questions/25055392/lambdas-local-variables-need-final-instance-variables-dont – isnot2bad Mar 13 '15 at 10:39
  • @fge I tried what you suggested, but the compiler accepts this: `public Supplier makeSupplier() { Supplier supp = () -> { return str; }; str = "ff"; return supp; }` – felixx Mar 13 '15 at 11:31
  • **DO READ** - https://stackoverflow.com/a/51850049/4691279 – hagrawal7777 Aug 14 '18 at 21:31

2 Answers2

7

We all agree that the first example won't work as local variables or parameters must be final or effectively final to be used within a lambda expression body.

But your second example does not involve local variables or parameters, as str is an instance field. Lambda expressions can accessing instance fields the same way as instance methods:

15.27.2. Lambda Body

A lambda body is either a single expression or a block (§14.2). Like a method body, a lambda body describes code that will be executed whenever an invocation occurs.

In fact, the java compiler creates a private method lambda$0 out of your lambda expression, that simply accesses the instance field str:

private java.lang.String lambda$0() {
    0 aload_0;                /* this */
    1 getfield 14;            /* .str */
    4 areturn;
}

Another point of view: You could also implement the Supplier using a plain-old anonymous inner class:

public Supplier<String> makeSupplier() {
    return new Supplier<String>() {
        public String get() { return str; }
    };
}

Accessing instance fields from inner classes is very common and not a speciality of Java 8.

isnot2bad
  • 24,105
  • 2
  • 29
  • 50
0

It does not. It allows access to effectively final class variables.

A variable or parameter whose value is never changed after it is initialized is effectively final.

Source: http://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html

Tobias Johansson
  • 378
  • 4
  • 11
  • I was almost about to agree. But in that case, the lambda expression is accessing an instance field which is neither a variable nor a parameter. – isnot2bad Mar 13 '15 at 10:41
  • Actually, the value accessed is a field. There is nothing that prevents access to non-final *fields*, as you can see in the document you quoted. – RealSkeptic Mar 13 '15 at 10:42
  • Future visitors - **DO READ** - https://stackoverflow.com/a/51850049/4691279 – hagrawal7777 Aug 14 '18 at 21:32