![](/img/trans.png)
[英]Why an object marked as final can be modified and call non-final method in Java?
[英]Why can method reference use non-final variables?
我对内部类和lambda表达有一些困惑,我试图提出一个问题 ,然后又出现了另一个疑问,并且可能更好地发布另一个问题,而不是评论前一个问题。
直截了当:我知道( 谢谢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
不是[有效]最终等等。
但是,这怎么允许的呢?
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
? 在第二种情况下,我是否面临“使用过时变量”的风险?
我的意思是,在后一种情况下,在one = new One();
执行f
还有一个彻头彻尾的最新副本one
(即引用旧的对象)。 这不是我们试图避免的那种模糊性吗?
方法引用不是lambda表达式,尽管它们可以以相同的方式使用。 我认为这就是造成混乱的原因。 下面是Java如何工作的简化,它不是真正的工作方式,但它足够接近。
假设我们有一个lambda表达式:
Runnable f = () -> one.bar();
这相当于实现Runnable
的匿名类:
Runnable f = new Runnable() {
public void run() {
one.bar();
}
}
这里适用的规则与匿名类(或方法本地类)相同。 这意味着, 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
引用。 但是参考是到对象 ,而不是可变的 。
这与传统的Java案例相同:
One one = new One();
One two = one;
one = new One();
two.bar();
所以,如果有什么one
变化? two
引用该对象one
曾经是,并且可以访问其方法。
另一方面,您的第一个示例是一个匿名类,它是一个经典的Java结构,可以引用它周围的局部变量。 代码引用实际变量one
,而不是它引用的对象。 由于Jon在您提到的答案中提到的原因,这是受限制的。 请注意,Java 8中的更改仅仅是变量必须是最终的 。 也就是说,初始化后仍然无法更改。 编译器变得非常复杂,即使未明确使用final
修饰符,也能确定哪些情况不会混淆。
共识似乎是因为当您使用匿名类执行它时, one
引用变量,而当您使用方法引用执行它时,在创建方法句柄时捕获one
值。 事实上,我认为在这两种情况下, one
是价值而不是变量。 让我们更详细地考虑匿名类,lambda表达式和方法引用。
匿名课程
请考虑以下示例:
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.
}
在这个例子中,我们调用toString
上o
方法之后getStringSupplier
又回来了,所以当它出现在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();
}
}
匿名类使它看起来好像使用局部变量,实际上捕获了这些变量的值。
与此相反,如果匿名类的方法引用封闭实例的字段,则不捕获这些字段的值,并且匿名类的实例不保存对它们的引用; 相反,匿名类包含对封闭实例的引用,并且可以访问其字段(直接或通过合成访问器,具体取决于可见性)。 一个优点是需要额外引用一个对象而不是几个对象。
Lambda表达式
Lambda表达式也会关闭值,而不是变量。 Brian Goetz撰写给出的理由在这里是
成语是这样的:
int sum = 0; list.forEach(e -> { sum += e.size(); }); // ERROR
基本上是连续的; 写这样没有竞争条件的lambda体是很困难的。 除非我们愿意强制执行 - 最好是在编译时 - 这样的函数无法逃脱其捕获线程,否则此功能可能会导致比它解决的更多麻烦。
方法参考
在创建方法句柄时方法引用捕获变量值的事实很容易检查。
例如,以下代码两次打印"a"
:
String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());
摘要
总而言之,lambda表达式和方法引用关闭值而不是变量。 在局部变量的情况下,匿名类也会关闭值。 在字段的情况下,情况更复杂,但行为基本上与捕获值相同,因为字段必须是有效的最终。
有鉴于此,问题是,为什么适用于匿名类和lambda表达式的规则不适用于方法引用,也就是说,当o
不是有效的最终时,为什么允许写o::toString
? 我不知道答案,但在我看来确实是一个不一致的地方。 我想这是因为你不能用方法参考做同样多的伤害; 像上面引用的lambda表达式的示例不适用。
在第一个示例中,您定义了F inline的实现,并尝试访问实例变量1。
在第二个示例中,您基本上将lambda表达式定义为对象之一的bar()
调用。
现在这可能有点令人困惑。 这种表示法的好处是你可以定义一个方法(大多数时候它是静态方法或静态上下文),然后从各种lambda表达式引用相同的方法:
msg -> System.out::println(msg);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.