英文:
Access variables from outer methods
问题
我正在学习Java中的内部类,遇到了与外部方法中变量引用相关的问题。例如,我有一个源代码用于统计在排序过程中调用了多少次compareTo()
方法:
int counter = 0;
Date[] dates = new Date[100];
for(int i = 0; i < dates.length; i++)
{
dates[i] = new Date()
{
public int compareTo(Date other)
{
counter++;
return super.compareTo(other);
}
};
}
Arrays.sort(dates);
System.out.println(counter + " comparisons");
执行此源代码时,可以看到在使用counter++
时存在错误。为了解决这个问题,有些人告诉我应该像这样更改:
int[] counter = new int[1];
Date[] dates = new Date[100];
for(int i = 0; i < dates.length; i++)
{
dates[i] = new Date()
{
public int compareTo(Date other)
{
counter[0]++;
return super.compareTo(other);
}
};
}
Arrays.sort(dates);
System.out.println(counter[0] + " comparisons");
我感到困惑,这两种代码之间有什么区别,以及为什么会出现此错误以及其解决方法是什么?
英文:
I am studying Inner Class in Java and I have a problem related to reference of variables in the outer methods. For example, I have a source code to count how many times compareTo()
methods is called during the sorting:
int counter = 0;
Date[] dates = new Date[100];
for(int i = 0; i < dates.length; i++)
{
dates[i] = new Date()
{
public int compareTo(Date other)
{
counter++;
return super.compareTo(other);
}
};
}
Arrays.sort(dates);
System.out.println(counter + " comparisons");
As executing the source code, you can see that there exists an error in the using of counter++
. To solve this problem, some people told me that I should change like this:
int[] counter = new int[1];
Date[] dates = new Date[100];
for(int i = 0; i < dates.length; i++)
{
dates[i] = new Date()
{
public int compareTo(Date other)
{
counter[0]++;
return super.compareTo(other);
}
};
}
Arrays.sort(dates);
System.out.println(counter[0] + " comparisons");
I am confused that what is the difference between these two codes and what is the reason for this error and its solution?
答案1
得分: 2
你正在创建一段可以“移动”的代码片段。位于{}
内的new Date()
声明中的代码不会在你编写它的位置立即运行;它附加到你创建的这个日期对象上,并随之一起移动。这个日期对象可以移动:它可以存储在一个字段中。也许它将在18天后运行,位于完全不同的线程中。虚拟机不知道这一点,因此需要为此做好准备。
那么假设它确实发生了:你的“counter”变量会发生什么?
通常,局部变量存储在“堆栈”上,并在方法退出时销毁。但在这种情况下,我们将销毁一个你的移动代码仍然可以访问的变量,那么在18天后,当你的日期compareTo代码被调用时会发生什么呢?
假设虚拟机在变量上默默地进行“升级”;而不是像正常情况下在堆栈上声明它,而是在堆上声明它,以便该变量可以在方法退出时继续存在。
好吧,如果compareTo在另一个线程中被调用怎么办?现在是否可以将局部变量标记为“volatile”?在Java中宣称,即使是局部变量也可能出现竞态条件,这是否可以?
这是一个判断性的问题;这是语言设计者需要决定的事情。
Java的语言设计者决定 不 将变量默默升级为堆,并 不 允许局部变量潜在地受到多线程访问的影响。
因此,你在任何可以“移动”的代码块中访问的任何局部变量都必须要么 [A] 被声明为 final
,要么 [B] 表现得好像已经是 final
,在这种情况下,Java将会默默地将其标记为final
。
改变的是第二个代码片段中的 counter
变量本身 不会发生变化:它是对数组的引用,而引用本身永远不会改变。实际上,你已经增加了间接级别和堆访问:数组存在于堆上。
就我个人而言,我认为使用AtomicX更可读。所以,如果你需要一个可以在移动代码中修改的整数,不要使用 new int[1]
;而要使用 new AtomicInteger
。如果你需要一个可修改的字符串,使用 new AtomicReference<String>()
,而不是 new String[1]
。
顺便说一句,在 这个 具体的代码中,计数器变量仅在此方法和计数器变量可以在此方法结束后消失,但编译器不会进行如此深入的分析来弄清楚这一点,它使用更简单的规则:想要从外部范围的移动代码中访问局部变量?不允许 - 除非它是(实际上)final。
*) 移动的代码是指在方法局部或匿名类定义内的任何内容,以及在lambda中的任何内容。因此:
void method() {
class MethodLocalClassDef {
// 这里的任何内容都被认为是“移动的”
}
Object o = new Object() {
// 这是匿名类定义,
// 这里的任何内容都被认为是“移动的”
};
Runnable r = () -> {
// 这是lambda,被认为是“移动的”
};
}
英文:
You're creating a snippet of code which can 'travel'. The code in the {}
that belong to your new Date()
declaration isn't run right where you wrote it; it is attached to this date object you've made, and goes with it. This date object can travel: It can be stored in a field. Maybe it is run 18 days from now, in a completely different thread. The VM has no idea, so it needs to be prepared for that to happen.
So let's say it does: What is to happen to your 'counter' variable?
Normally, local variables are stored 'on the stack' and are destroyed as the method exits. But in that case we'd be destroying a variable that your travelling code still has access to, so what does that mean, 18 days from now, when your date compareTo code is invoked?
Let's say that the VM silently 'upgrades' the variables; instead of declaring it on the stack like normal, it declares it on the heap so that the variable can survive the method exiting.
Allright. What if compareTo is invoked in another thread? Should it now be possible to mark a local variable as 'volatile'? Is it okay to state that, in java, even local variables may show race conditions?
This is a judgemental call; something for the language designers to decide.
Java's language designers decided against silent upgrade into heap and against allowing locals to potentially be subjected to multi-thread access.
Therefore, any local variable that you access in any codeblock that can 'travel'* must either [A] be declared final
or [B] act as if it could have been, in which case java will silently make it final for you.
The change is that counter
, the variable itself, does not change in the second snippet: it is a reference to an array, and that reference never changes. Effectively you've added the level of indirection and the heap access yourself: arrays exist on the heap.
For what its worth, I find usage of AtomicX more readable. So if you need an int that is modifyable in traveling code, don't do new int[1]
; do new AtomicInteger
. If you need a modifiable string, use new AtomicReference<String>()
, not new String[1]
.
NB: Yes, in this specific code, the counter variable is only used, even by the sort op, within this method and the counter var can go away once this method ends, but the compiler doesn't do that kind of extremely in depth analysis to figure that out, it uses the much simpler rule of: Wanna access a local var from outer scipe within 'travelling' code? Not allowed - unless it is (effectively) final.
*) Traveling code is anything inside a method local or anonymous class definition, and anything in a lambda. So:
void method() {
class MethodLocalClassDef {
// anything here is considered 'travelling'
}
Object o = new Object() {
// this is an anonymous class def,
// and anything in here is 'travelling'
};
Runnable r = () -> {
// this is a lambda, and considered 'travelling'
};
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论