lambda 表达式和实例化方法引用之间的不同行为

Different behavior between lambda expression and method reference by instantiation

据我所知,lambda 表达式可以被方法引用替换,没有任何问题。我的 IDE 说的是一样的,但下面的例子显示了相反的情况。 该方法清楚地引用 returns 同一个对象,其中 lambda 表达式每次 returns 新对象。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Instance {

    int member;

    Instance set(int value){
        this.member = value;
        return this;
    }

    @Override
    public String toString() {
        return member + "";
    }

    public static void main(String[] args) {

        Stream<Integer> stream1 = Stream.of(1, 2, 3, 4);
        Stream<Integer> stream2 = Stream.of(1, 2, 3, 4);

        List<Instance> collect1 = stream1.map(i -> new Instance().set(i)).collect(Collectors.toList());
        List<Instance> collect2 = stream2.map(new Instance()::set).collect(Collectors.toList());

        System.out.println(collect1);
        System.out.println(collect2);
    }
}

这是我的输出:

[1, 2, 3, 4]
[4, 4, 4, 4]

您的 lambda 表达式每次执行时都会调用 new Instance()。这解释了为什么其 toString() 的结果对于每个元素都不同。

方法引用保留引用它的实例,因此它类似于:

Instance instance = new Instance();
List<Instance> collect2 = stream2.map(instance::set).collect(Collectors.toList());

本例使用方法引用的结果是同一个实例调用set,最后收集。 member显示的值是最后一组。


作为实验,进行这些更改并观察实例在 lambda 表达式的情况下发生变化:

/* a random string assigned per instance */
private String uid = UUID.randomUUID().toString();

Instance set(int value) {
    this.member = value;
    System.out.println("uid: " + uid); //print the ID
    return this;
}

第二个选项的不同之处在于,您在创建 Stream 管道时创建了一个 (1) 实例。当您在调用终端方法 (toList) 后最终迭代流元素时,您会在同一实例上调用 set 方法四次,其中最后一个值是最后一个值。结果列表 (collect2) 包含 相同 实例的四倍。

在第一个中,对于流中的每个项目,map() 中的 lambda 表达式正在创建一个新的 Instance 对象。

在第二个中,new Instance()map() 开始传递值之前被调用一次。

如果要使用方法引用,请像这样向 Instance 添加构造函数。 (我实际上还建议通过使 member final 使 Instance 不可变,这样您就可以避免在其他地方像这样混淆)。

private final int member;

public Instance(int member) {
  this.member = member;
}

//remove the setter

然后将流处理更改为如下所示:

List<Instance> collect2 = stream2.map(Instance::new).collect(Collectors.toList());

这样你就可以确保成员一旦初始化就不会改变,并且你可以简洁地使用方法引用(在这种情况下,构造函数是带有new的方法引用)。

方法引用表达式求值的时间不同于 哪个 lambda 表达式。
对于在 :: 之前具有表达式(而不是类型)的方法引用,子表达式会立即求值,然后存储并重用求值结果。
所以在这里:

new Instance()::set

new Instance() 计算一次。

来自 15.12.4. Run-Time Evaluation of Method Invocation(强调是我的):

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.