为什么 java 允许 class 级变量在匿名内部 class 中重新分配,而局部变量则不允许
Why does java allow class level variables to be reassigned in anonymous inner class, whereas same is not allowed for local variables
这个问题类似于 Lambdas: local variables need final, instance variables don't,但唯一的区别是这个问题即使没有 lambda 表达式也有效,即即使在 Java7.
上也有效
下面是代码片段。
public class MyClass {
Integer globalInteger = new Integer(1);
public void someMethod() {
Integer localInt = new Integer(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
globalInteger = new Integer(11);//no error
localInt = new Integer(22);//error here
}
};
}
}
我可以为 globalInteger 重新分配一个新值,但不能为 localInteger。为什么会有这种差异?
因为 lambda 函数不是 class 的一部分。
考虑以下代码(您的小改动):
public Runnable func() {
Integer localInt = new Integer(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
globalInteger = new Integer(11);//no error
localInt = new Integer(22);//error here
}
};
return runnable;
}
//Somewhere in the code:
Runnable r = func();
r.run(); // At this point localInt is not defined.
要理解为什么允许非局部变量改变,我们首先需要理解为什么不允许局部变量。那是因为局部变量存储在堆栈中(实例(或静态)变量不是)。
堆栈变量的问题是一旦它们包含方法returns,它们就会消失。但是,您的匿名 class 的实例可能会比这更长寿。因此,如果访问局部变量是天真地实现的,则在方法返回后使用内部 class 内部的局部变量将访问堆栈帧上不再存在的变量。这将导致崩溃、异常或未定义的行为,具体取决于具体的实现。由于这显然很糟糕,因此通过复制来实现对局部变量的访问。也就是说,class使用的所有局部变量(包括特殊变量this
)都被复制到匿名对象中。因此,当内部 class 的方法访问局部变量 x
时,它实际上并没有访问该局部变量。它正在访问存储在对象中的副本。
但是,如果局部变量在创建对象后发生更改,或者对象的方法更改了变量,会发生什么情况?那么,前者会导致局部变量改变,但不会改变对象中的副本,而后者会改变副本,但不会改变原始对象。因此,无论哪种方式,变量的两个版本都将不再相同,这对于任何不知道正在进行的复制的程序员来说都是非常违反直觉的。所以为了避免这个问题,只有当局部变量的值永远不会改变时,你才被允许访问它们。
不需要复制实例变量,因为它们不会消失,直到它们包含的对象被垃圾回收(并且静态变量永远不会消失)——因为匿名对象将包含对外部对象的引用this
,在匿名对象也被垃圾回收之前,这不会发生。因此,由于它们未被复制,因此修改它们不会导致任何问题,也没有理由禁止它。
编译器告诉你内部 class 中的变量必须是最终变量或有效最终变量的错误。这是由于 Java 的 8 闭包以及 JVM 捕获引用的方式。限制是在 lambda 主体中捕获的引用必须是最终的(不可重新分配),并且编译器需要确保它不引用局部变量的副本。
因此,如果您访问实例变量,您的 lambda 实际上是在引用周围 class 的 this
实例,这是有效的最终(不变引用)。此外,如果您使用包装器 class 或数组,编译器错误也会消失。
因为 JVM 没有机器指令来分配位于与当前堆栈帧不同的任何堆栈帧中的变量。
这个问题类似于 Lambdas: local variables need final, instance variables don't,但唯一的区别是这个问题即使没有 lambda 表达式也有效,即即使在 Java7.
上也有效下面是代码片段。
public class MyClass {
Integer globalInteger = new Integer(1);
public void someMethod() {
Integer localInt = new Integer(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
globalInteger = new Integer(11);//no error
localInt = new Integer(22);//error here
}
};
}
}
我可以为 globalInteger 重新分配一个新值,但不能为 localInteger。为什么会有这种差异?
因为 lambda 函数不是 class 的一部分。
考虑以下代码(您的小改动):
public Runnable func() {
Integer localInt = new Integer(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
globalInteger = new Integer(11);//no error
localInt = new Integer(22);//error here
}
};
return runnable;
}
//Somewhere in the code:
Runnable r = func();
r.run(); // At this point localInt is not defined.
要理解为什么允许非局部变量改变,我们首先需要理解为什么不允许局部变量。那是因为局部变量存储在堆栈中(实例(或静态)变量不是)。
堆栈变量的问题是一旦它们包含方法returns,它们就会消失。但是,您的匿名 class 的实例可能会比这更长寿。因此,如果访问局部变量是天真地实现的,则在方法返回后使用内部 class 内部的局部变量将访问堆栈帧上不再存在的变量。这将导致崩溃、异常或未定义的行为,具体取决于具体的实现。由于这显然很糟糕,因此通过复制来实现对局部变量的访问。也就是说,class使用的所有局部变量(包括特殊变量this
)都被复制到匿名对象中。因此,当内部 class 的方法访问局部变量 x
时,它实际上并没有访问该局部变量。它正在访问存储在对象中的副本。
但是,如果局部变量在创建对象后发生更改,或者对象的方法更改了变量,会发生什么情况?那么,前者会导致局部变量改变,但不会改变对象中的副本,而后者会改变副本,但不会改变原始对象。因此,无论哪种方式,变量的两个版本都将不再相同,这对于任何不知道正在进行的复制的程序员来说都是非常违反直觉的。所以为了避免这个问题,只有当局部变量的值永远不会改变时,你才被允许访问它们。
不需要复制实例变量,因为它们不会消失,直到它们包含的对象被垃圾回收(并且静态变量永远不会消失)——因为匿名对象将包含对外部对象的引用this
,在匿名对象也被垃圾回收之前,这不会发生。因此,由于它们未被复制,因此修改它们不会导致任何问题,也没有理由禁止它。
编译器告诉你内部 class 中的变量必须是最终变量或有效最终变量的错误。这是由于 Java 的 8 闭包以及 JVM 捕获引用的方式。限制是在 lambda 主体中捕获的引用必须是最终的(不可重新分配),并且编译器需要确保它不引用局部变量的副本。
因此,如果您访问实例变量,您的 lambda 实际上是在引用周围 class 的 this
实例,这是有效的最终(不变引用)。此外,如果您使用包装器 class 或数组,编译器错误也会消失。
因为 JVM 没有机器指令来分配位于与当前堆栈帧不同的任何堆栈帧中的变量。