Java 8 Method references called on a local variabl

2019-02-02 22:26发布

问题:

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.

回答1:

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.



回答2:

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



回答3:

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.