【问题标题】:Java 8 Method references called on a local variable在局部变量上调用的 Java 8 方法引用
【发布时间】:2015-03-01 02:48:11
【问题描述】:

我正在学习 Java 8,遇到了一些我觉得有点奇怪的东西。

考虑以下 sn-p:

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())
    );
}

基本上,我需要将名为 relationships 的输入集映射到不同的类型,以便 符合我正在使用的 DAO 的 API。对于转换,我想使用现有的 RelationshipTransformerImpl 类,我将其实例化为局部变量。

现在,这是我的问题:

如果我将上面的代码修改如下:

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
}

我显然会得到一个编译错误,因为局部变量transformer 不再是“有效的最终”。但是,如果将 lambda 替换为方法引用:

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
}

然后我不再收到编译错误!为什么会这样?我认为编写 lambda 表达式的两种方法应该是等价的,但显然还有更多事情要做。

【问题讨论】:

    标签: java lambda java-8 method-reference


    【解决方案1】:

    猜想,但对我来说,这就是发生的事情......

    编译器根本无法断言创建的流是同步的;它认为这是一种可能的情况:

    • relationships 参数创建流;
    • 重新影响transformer;
    • 流展开。

    编译时生成的是调用站点;它仅在流展开时才链接。

    在您的第一个 lambda 中,您引用了一个局部变量,但该变量不是调用站点的一部分。

    在第二个 lambda 中,由于您使用方法引用,这意味着生成的调用站点必须保留对该方法的引用,因此类实例持有该方法。它是由您之后更改的局部变量引用的事实并不重要。

    我的两分钱...

    【讨论】:

      【解决方案2】:

      JLS 15.13.5可以解释:

      方法引用表达式求值的时间比 lambda 表达式更复杂(第 15.27.4 节)。当方法引用表达式在 :: 分隔符之前有一个表达式(而不是类型)时,会立即计算该子表达式。评估的结果被存储,直到调用相应的功能接口类型的方法;此时,结果将用作调用的目标引用。这意味着 :: 分隔符之前的表达式仅在程序遇到方法引用表达式时才被评估,并且不会在函数接口类型的后续调用中重新评估。

      据我了解,由于在您的情况下 transformer 是 :: 分隔符之前的表达式,因此它只计算一次并存储。由于不必为了调用引用的方法而重新评估它,所以 transformer 稍后被分配为 null 并不重要。

      【讨论】:

      • @SotiriosDelimanolis lambda 是 not 立即评估的;它只是在编译时生成的调用站点。实际链接仅在 lambda 第一次为 invokedynamiced 时发生。
      • @SotiriosDelimanolis 我确实的意思是被调用了。据我了解,没有什么能保证 lambda 会立即被调用。这意味着编译器需要确保在链接完成时引用仍然有效。评估 lambda 只会创建调用站点,不会链接它。
      • @SotiriosDelimanolis 但 lambda 的行为不同;它们是 indy 调用站点,因此它们使用仅在实际执行链接时才调用的引导方法。这与匿名类的作用非常不同。编译器唯一要做的就是生成这个引导方法。
      • @SotiriosDelimanolis "什么是引导方法,什么是链接?具体来说" java.lang.invoke API。 bootstrap 方法 描述了一个callsiteinvokedynamic 将查询它将调用站点链接到实际代码。链接完成后,引导方法将不碍事,除非调用站点发生变化。但是在 Java 中,它永远不会发生,因为 Java 是静态类型的。例如,在 Scala 中情况并非如此,其中可以使用不同的参数类型调用某些方法。
      • 捕获的变量和捕获的方法引用目标在字节码级别没有区别。这是一个语言设计决策,只有有效的最终变量才能被捕获。我认为 cmets 部分不适合讨论语言设计决策……
      【解决方案3】:

      在您的第一个示例中,每次调用映射函数时都会引用 transformer,因此对于每个关系一次。

      在第二个示例中,transformer 仅被引用一次,此时 transformer::transformRelationship 被传递给 map()。所以以后改不改也没关系。

      这些不是“编写 lambda 表达式的两种方式”,而是 lambda 表达式和方法引用,这是该语言的两个不同特征。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多