Java中匿名内部类中局部变量的可见性 - 为什么需要使用'final'关键字?

25
我不明白为什么我不能总是从“监听器”或“处理程序”内部访问变量。
这是我的代码:
Button btnDownload = new Button(myparent, SWT.NONE);
btnDownload.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                btnDownload.setEnabled(false); // I CAN'T 
            }
        });

唯一的方法是使用 final 关键字进行声明:
final Button btnDownload = new Button(myparent, SWT.NONE);

为什么我需要将变量声明为final才能在事件内部访问?

2
另一种方法是将btnDownnload提升为类字段而不是局部变量。 - Bala R
4个回答

45

您的SelectionAdapter是一个匿名内部类,我认为这一点很清楚:

局部类确实可以引用实例变量。它们不能引用非final局部变量的原因是,局部类实例在方法返回后可能仍然存在于内存中。当方法返回时,局部变量超出了作用域,所以需要它们的副本。如果这些变量不是final的,那么方法中变量的副本可能会发生变化,而局部类中的副本没有变化,因此它们将不同步。

Java中实现匿名内部类的方式要求使用final变量。匿名内部类(AIC)通过创建一个私有实例字段来使用局部变量,该字段保存局部变量值的副本。内部类实际上并没有使用局部变量,而是使用了一个副本。此时应该很明显了,如果原始值或复制值发生更改,则可能会发生“坏事”™导致一些意外的数据同步问题。为了防止这种问题的发生,Java要求您将将被AIC使用的局部变量标记为final(即不可更改)。这保证了内部类对局部变量的副本始终与实际值相匹配。


哇..比之前更困惑了!;) 所以,我问你:如果我声明需要访问的变量为final,我会做对的事情吗? - stighy
1
@stighy 是的。你必须确保局部变量在超出作用域后仍保持不变,因为匿名类可能会留在内存中,正如之前所说。你真的不希望出现可能意外改变的变量。我表达清楚了吗,还是像之前一样让人困惑? :) - user219882

18

我认为Tom的意思是,如果您可以在匿名类中使用局部变量,则以下代码段应启用哪个按钮。

    Button btnDownload = new Button(myparent, SWT.NONE);
    btnDownload.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    btnDownload.setEnabled(false); // I CAN'T 
                }
            });
   btnDownload = new Button(somethingelse , SWT.NONE);
   btnDownload = null ;

Java的创造者想要避免这种争论,因此要求在匿名类中使用的局部变量必须是final的。

这样做可以防止创建匿名内部类的对象被垃圾回收。 - sourcedelica
+100我从未见过如此自解释的问题示例。 - M Faisal Hameed

3

这可能不是最佳的设计选择。在Java 8中,随着lambda表达式的引入,希望能够取消对final的要求。

目标是禁止从匿名类分配给局部变量。但这并不需要将局部变量标记为final。

void f()

  Object var = ...;
  new Anon(){
      ...
      print(var); // reading is ok
      var = x;    // error, can't write to var            [1]
  }

编译器实际上会复制变量并将其保存在匿名类中,匿名类之后只访问该副本。上面的代码实际上被转换为:
void f()

  Object var = ...;
  new Anon$1(var);

class Anon$1
{
    final Object $var;
    Anon$1(Object var){ this.$var=var; }
    ...
    print($var); 
    $var = x;    // error, naturally                      [2]
}

正如您所看到的,没有技术上的理由要求var是final的。编译器遇到[2]时,只需知道$varvar的合成字段,并报告错误“无法通过匿名类为局部变量var赋值”(而不是“$var是final的,不能被赋值”)。
语言设计者选择通过在局部变量上添加final关键字来使我们感到恼火;我不记得其中的原理;总的来说,如果需要清晰度,Java并不害怕冗长。

-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接