为什么方法引用可以使用非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::toString
时o
不是最终有效的吗?我不知道这个问题的答案,但在我看来这确实是一种矛盾。我想这是因为你不能用方法引用造成那么大的伤害;上面为 lambda 表达式引用的示例不适用。
我对内部 类 和 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
:
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::toString
时o
不是最终有效的吗?我不知道这个问题的答案,但在我看来这确实是一种矛盾。我想这是因为你不能用方法引用造成那么大的伤害;上面为 lambda 表达式引用的示例不适用。