27

I am in the process of learning Java 8 and I came across something that I find a bit strange.

Consider the following snippet:

private MyDaoClass myDao;

public void storeRelationships(Set<Relationship<ClassA, ClassB>> relationships) {
    RelationshipTransformer transformer = new RelationshipTransformerImpl();

    myDao.createRelationships(
            relationships.stream()
            .map((input) -> transformer.transformRelationship(input))
            .collect(Collectors.toSet())
    );
}

Basically, I need to map the input set called relationships to a different type in order to conform to the API of the DAO I'm using. For the conversion, I would like to use an existing RelationshipTransformerImpl class that I instantiate as a local variable.

Now, here's my question:

If I was to modify the above code as follows:

public void storeRelationships(Set<Relationship<ClassA, ClassB>> relationships) {
    RelationshipTransformer transformer = new RelationshipTransformerImpl();

    myDao.createRelationships(
            relationships.stream()
            .map((input) -> transformer.transformRelationship(input))
            .collect(Collectors.toSet())
    );

    transformer = null;  //setting the value of an effectively final variable
}

I would obviously get a compilation error, since the local variable transformer is no longer "effectively final". However, if replace the lambda with a method reference:

public void storeRelationships(Set<Relationship<ClassA, ClassB>> relationships) {
    RelationshipTransformer transformer = new RelationshipTransformerImpl();

    myDao.createRelationships(
            relationships.stream()
            .map(transformer::transformRelationship)
            .collect(Collectors.toSet())
    );

    transformer = null;  //setting the value of an effectively final variable
}

Then I no longer get a compilation error! Why does this happen? I thought the two ways to write the lambda expression should be equivalent, but there's clearly something more going on.

0

3 Answers 3

25

JLS 15.13.5 may hold the explanation:

The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.

As I understand it, since in your case transformer is the expression preceding the :: separator, it is evaluated just once and stored. Since it doesn't have to be re-evaluated in order to invoke the referenced method, it doesn't matter that transformer is later assigned null.

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

12 Comments

@SotiriosDelimanolis the lambda is not evaluated immediately; it is only the call site which is generated at compile time. The actual linkage only happens the first time the lambda is invokedynamiced.
@SotiriosDelimanolis I do mean invoked. From what I understand, nothing guarantees that the lambda will be invoked at once. Which means that the compiler needs to ensure that the reference is still valid when the linkage is done. Evaluating the lambda only creates the call site, it does not link it.
@SotiriosDelimanolis but lambdas behave differently; they are indy call sites, therefore they use a bootstrap method which is called only when the linkage is actually performed. This is very different from what an anonymous class does. The only thing that the compiler does is generate this bootstrap method.
@SotiriosDelimanolis " What is a bootstrap method, what is linkage? Concretely" <-- concretely, this is JSR 292. While it does not cover it all, an example is the java.lang.invoke API. A bootstrap method is what describes a callsite; it will be queried by invokedynamic to link the callsite to actual code. When the linkage is done, the bootstrap method gets out of the way, unless the callsite changes. But in Java, it never happens since Java is statically typed. This is not the case in Scala for instance, where some methods can be called with different argumen types.
There is no difference between captured variables and captured method reference targets at the byte code level. It is a language design decision that only effectively final variables can be captured. I don’t think that the comments section is an appropriate place to discuss language design decisions…
|
5

Wild guess but to me, here is what happens...

The compiler cannot assert that the created stream is synchronous at all; it sees this as a possible scenario:

  • create stream from relationships argument;
  • reaffect transformer;
  • stream unrolls.

What is generated at compile time is a call site; it is linked only when the stream unrolls.

In your first lambda, you refer to a local variable, but this variable is not part of the call site.

In the second lambda, since you use a method reference, it means the generated call site will have to keep a reference to the method, therefore the class instance holding that method. The fact that it was referred by a local variable which you change afterwards does not matter.

My two cents...

Comments

5

In your first example, transformer is referenced every time the mapping function is called, so once for every relationship.

In your second example transformer is referenced only once, when transformer::transformRelationship is passed to map(). So it doesn't matter if it changes afterward.

Those are not "the two ways to write the lambda expression" but a lambda expression and a method reference, two distinct features of the language.

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.