为什么方法引用可以使用非final变量?

Why can method reference use non-final variables?

我对内部 类 和 lambda 表达式有些困惑,我试图就此问一个 ,但随后出现了另一个疑问,发布另一个问题可能比评论上一个。

开门见山:我知道 (thank you Jon) 这样的东西不会编译

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = new F(){      //1
            public void foo(){one.bar();}   //compilation error
        };

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

由于 Java 管理闭包的方式,因为 one 不是 [有效] final 等等。

可是,怎么会允许呢?

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = one::bar; //2

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

//2不等于//1吗?在第二种情况下,我不是面临"working with an out-of-date variable"的风险吗?

我的意思是,在后一种情况下,在执行 one = new One(); 之后 f 仍然有一个 one 的过时副本(即引用旧对象)。这不就是我们要避免的歧义吗?

没有。在您的第一个示例中,您定义了 F 内联的实现并尝试访问实例变量 one.

在第二个示例中,您基本上将 lambda 表达式定义为对对象一的 bar() 的调用。

现在这可能有点令人困惑。这种表示法的好处是您可以定义一个方法(大多数情况下它是静态方法或在静态上下文中)一次,然后从各种 lambda 表达式中引用相同的方法:

msg -> System.out::println(msg);

方法引用不是 lambda 表达式,尽管它们可以以相同的方式使用。我认为这就是造成混乱的原因。下面是 Java 工作原理的简化,它不是真正的工作原理,但已经足够接近了。

假设我们有一个 lambda 表达式:

Runnable f = () -> one.bar();

这相当于实现 Runnable:

的匿名 class
Runnable f = new Runnable() {
    public void run() {
       one.bar();
    }
}

此处适用与匿名 class(或本地方法 class)相同的规则。这意味着 one 需要有效地完成它才能工作。

另一方面,方法句柄:

Runnable f = one::bar;

更像是:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar"));

MethodHandle 为:

public class MethodHandle implements Runnable {
    private final Object object;
    private final Method method;

    public MethodHandle(Object object, java.lang.reflect.Method method) {
        this.object = Object;
        this.method = method;
    }

    @Override
    public void run() {
        method.invoke(object);
    }
}

在这种情况下,分配给 one 的对象是作为创建的方法句柄的一部分进行分配的,因此 one 本身不需要有效地完成此工作。

你的第二个例子根本就不是lambda表达式。这是一个方法参考。在这种特殊情况下,它从当前由变量 one 引用的特定对象中选择一个方法。但是引用的是object,而不是变量one.

这与 classical Java 案例相同:

One one = new One();
One two = one;
one = new One();

two.bar();

如果 one 改变了呢? two 引用了 one 曾经是的对象,并且可以访问它的方法。

另一方面,您的第一个示例是匿名 class,它是一个 classical Java 结构,可以引用它周围的局部变量。该代码引用了实际变量 one,而不是它所引用的对象。由于乔恩在您提到的答案中提到的原因,这是受到限制的。请注意,Java 8 中的更改仅仅是变量必须 有效最终。也就是初始化之后还是不能改变。编译器变得足够复杂,可以确定哪些情况不会混淆,即使没有明确使用 final 修饰符。

大家一致认为这是因为当你使用匿名 class 时,one 指的是一个变量,而当你使用方法引用时,值one 在创建方法句柄时被捕获。事实上,我认为在这两种情况下 one 都是一个值而不是一个变量。让我们更详细地考虑匿名 classes、lambda 表达式和方法引用。

匿名 classes

考虑以下示例:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new Supplier<String>() {
        @Override
        public String get() {
            return o.toString();
        }
    };
}

public static void main(String[] args) {
    Supplier<String> supplier = getStringSupplier();
    System.out.println(supplier.get());  // Use o after the getStringSupplier method returned.
}

在此示例中,我们在方法 getStringSupplier 返回后在 o 上调用 toString,因此当它出现在 get 方法中时,o 不能引用 getStringSupplier 方法的局部变量。事实上它本质上等同于:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new StringSupplier(o);
}

private static class StringSupplier implements Supplier<String> {
    private final Object o;

    StringSupplier(Object o) {
        this.o = o;
    }

    @Override
    public String get() {
        return o.toString();
    }
} 

匿名 classes 使它 看起来 就好像您在使用局部变量,而实际上这些变量的值是被捕获的。

与此相反,如果匿名 class 的方法引用封闭实例的字段,则不会捕获这些字段的值,而匿名 class 的实例会 保留对它们的引用;相反,匿名 class 持有对封闭实例的引用并可以访问其字段(直接或通过合成访问器,取决于可见性)。一个优点是需要额外引用一个对象,而不是多个对象。

Lambda 表达式

Lambda 表达式也关闭值,而不是变量。 Brian Goetz here 给出的原因是

idioms like this:

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR

are fundamentally serial; it is quite difficult to write lambda bodies like this that do not have race conditions. Unless we are willing to enforce -- preferably at compile time -- that such a function cannot escape its capturing thread, this feature may well cause more trouble than it solves.

方法参考

创建方法句柄时方法引用捕获变量值的事实很容易检查。

例如,下面的代码打印两次"a"

String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());

总结

所以总而言之,lambda 表达式和方法引用关闭值,而不是变量。在局部变量的情况下,匿名 classes 也会关闭值。在字段的情况下,情况更复杂,但行为本质上与捕获值相同,因为字段必须有效地是最终的。

鉴于此,问题是,为什么适用于匿名 classes 和 lambda 表达式的规则不适用于方法引用,即为什么允许你写 o::toStringo 不是最终有效的吗?我不知道这个问题的答案,但在我看来这确实是一种矛盾。我想这是因为你不能用方法引用造成那么大的伤害;上面为 lambda 表达式引用的示例不适用。