3

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

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

...没有做a最后的。我得到了技术原因,这是因为局部变量存在于堆栈中,除非您知道它不会更改,否则您无法安全地制作副本。

然而,我很难看到的是为什么编译器没有实现黑客,所以当它看到上述情况时,它会编译成如下内容:

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

然后我们就可以安全地从匿名内部类访问 a 并且如果我们愿意的话确实可以更改它。当然,它可能仅在我们实际更改a. 据我所知,这将是一个相对简单的东西,适用于所有类型并且允许a从任何上下文中改变。当然,上述提议可以更改为对多个值使用合成包装类或其他更有效的方法,但想法是相同的。我猜对性能的影响很小,但我怀疑这会过度,尤其是在引擎盖下有更多优化的潜力。除了可能依赖于合成字段的某些反射调用以某种方式破坏之外,我看不到很多缺点,但我从未听说过它被认真提出!有什么原因吗?

4

3 回答 3

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 采用了限制性但简单的方法。

(使用数组而不是自定义类对单个变量有效,但当涉及多个变量时会变得更加浪费 -如果可以提供帮助,您真的不想为每个变量创建一个包装器对象。)

请注意,至少在使用 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++;
        }
    });
}

创建了多少“内部”变量?每个循环实例一个,还是一个整体?基本上,示波器让这类事情变得棘手。可行,但很棘手。

于 2012-01-20T14:44:15.790 回答
1

您遇到的另一个问题(并且可以在没有此限制的 groovy 中遇到)是非最终变量可以更改(否则您将不会有问题使其成为最终变量)

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

线程应该总是看到 a = 1 还是有时会看到 a = 2。

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

顺便说一句:我的 IDE 具有自动修复功能的另一种方法是使用一个数组来代替。

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

虽然丑陋,但它确实使您不太可能意外编写一个值可能会更改的匿名类。

于 2012-01-22T08:17:45.927 回答
0

鉴于匿名内部类的寿命可以超过创建它们的范围,我看不出你的“黑客”如何容易实现。内部类仍然需要维护它关闭的所有变量的副本,因此它们不能引用已在堆栈上创建的变量。

于 2012-01-20T14:51:44.030 回答