从匿名内部类访问时,为什么要求局部变量是最终的?

时间:2012-01-20 14:39:07

标签: java oop anonymous-inner-class

我们都知道你做不到这样的事情:

int a = 7;
new Runnable() {
     public void run() {
         System.out.println(a);
     }
}.run();
...

...没有让a最终成功。我得到了技术原因,这是因为局部变量存在于堆栈中,除非您知道它不会改变,否则您无法安全地复制。

然而,我很难看到编译器没有实现hack的原因,所以当它看到上述情况时它会编译成类似的东西:

int[] a = {7};
new Runnable() {
    public void run() {
        System.out.println(a[0]);
    }
}.run();
...

然后我们处于从匿名内部类访问一个安全的位置,如果我们愿意,它确实会改变它。当然,当我们实际更改a时,它可能只会执行此操作。据我所知,这将是一个相对简单的东西,适用于所有类型,并允许从任何上下文更改a。当然,上面的提议可以改为使用合成包装类来实现多个值,或者使用另一种更有效的方法,但这个想法是一样的。我想这会有很小的性能损失,但我怀疑它是否会过度,尤其是潜在的更多优化。除了某些依赖合成领域的反射性调用是某种方式破坏之外,我看不出很多缺点,但我从来没有听过它认真的提议!有原因吗?

3 个答案:

答案 0 :(得分:2)

构造匿名内部类时,将复制其中使用的所有变量的。因此,如果内部类尝试更改变量的值,那么将不会可见。例如,假设这是有效的:

int a = 7;
Runnable r = new Runnable() {
    public void run() {
        a = 5;
    }
};
r.run();
System.out.println(a);

你可能期望它打印5(确实它会在C#中)。但是因为只有一个副本,它实际上会打印7 ...如果它被允许,没有更大的变化。

当然,Java 可以更改为真正捕获变量而不是其值(因为C#用于匿名函数)。这需要自动创建一个额外的类来存储“本地”变量,并使方法和匿名内部类共享该额外类的实例。这会使匿名内部类更多强大,但可能更难理解。 C#决定采用功率复杂的路线; Java采用了限制性但简单的方法。

(使用数组而不是自定义类对单个变量有效,但当涉及多个变量时变得更加浪费 - 您实际上不希望为每个变量创建一个包装器对象< / em>如果你能提供帮助的话。)

请注意,捕获变量方法涉及很多复杂性,至少使用C#规则。例如:

List<Runnable> runnables = new ArrayList<Runnable>();
int outer = 0;
for (int i = 0; i < 10; i++) {
    int inner = 0;
    runnables.add(new Runnable() {
        public void run() {
            outer++;
            inner++;
        }
    });
}

创建了多少“内部”变量?一个循环的每个实例,或一个整体?基本上,范围使得生活变得棘手。可行,但很棘手。

答案 1 :(得分:1)

你遇到的另一个问题(并且可能没有这个限制的groovy)是非最终变量可以改变(否则你不会有问题使它成为最终的)

int a = 1; 
// use a in another thread, or potentially run it later
a = 2; 

如果线程总是看到a = 1,或者它有时会看到a = 2.

编写可预测的匿名类的唯一方法是局部变量是否不会改变。编译器可以更智能,并检测变量何时不能更改,这将简化一些奇怪的情况恕我直言。但最简单的解决方案是要求局部变量是最终的。

BTW:我的IDE有一个自动修复的替代方法是使用一个数组而不是。

final int[] a = { 1 };
//  use a[0] in an anonymous class.
a[0] = 2;

虽然很难看,但它确实会让你不小心写出一个值可能会改变的匿名类。

答案 2 :(得分:0)

鉴于匿名内部类可以比创建它们的范围更长,我不明白你是如何“破解”容易实现的。内部类仍然需要维护它已经关闭的所有变量的副本,因此它们不能引用已经在堆栈上创建的变量。