为什么Java内部类需要“final”外部实例变量?

65
final JTextField jtfContent = new JTextField();
btnOK.addActionListener(new java.awt.event.ActionListener(){
    public void actionPerformed(java.awt.event.ActionEvent event){
        jtfContent.setText("I am OK");
    }
} );

如果我省略 final,我会看到错误 "Cannot refer to a non-final variable jtfContent inside an inner class defined in a different method"。

为什么匿名内部类要求外部类的实例变量必须是 final 才能访问它?


4
我注意到这个问题比它所声称的重复问题要旧。 - Raedwald
2
根据相关的元讨论,@Raedwald,问题的时间并不重要:我应该投票关闭一个较新的重复问题吗? - “如果新问题是一个更好的问题或者有更好的答案,那么就投票将旧问题作为新问题的重复关闭...” - gnat
3
在Java 8中,允许传递编译器的变量是final或者有效的final - Victor Wong
@bruno 这不应该是针对'local'变量或方法参数而不是外部的'instance'变量吗? - sactiw
@sactiw 正确。标题误导了,没有人在回答中提到这一点。请问有人能把它修正一下吗? - rents
显示剩余2条评论
5个回答

81

首先,让我们都放松下来,并请放下那支枪。

好的。现在,语言之所以坚持这样做,是因为它通过欺骗来提供内部类函数访问他们渴望的局部变量的方式。运行时会复制局部执行上下文的副本(等等),因此它坚持要求您将所有内容设置为final,以便它保持事情的诚实性。

如果它不这样做,那么在构建对象后但在内部类函数运行之前更改局部变量值的代码可能会令人困惑和奇怪。

这就是围绕Java和“闭包”出现很多争议的精髓所在。


注意:开头的段落是对原始作品中一些大写字母文本的玩笑引用。


2
@j2emanue 这是一个好问题;我强烈怀疑这是一个浅拷贝(所以,如果你有一个对可变对象的final引用,那么通过伪闭包中的代码,该对象将是可变的)。进行深拷贝通常是一个困难的问题,我认为它以这种方式工作的可能性极小。 - Pointy
2
这绝对是一个浅拷贝。Java 中没有神奇的“深拷贝”机制。 - DavidS
1
是的,在Java中,原始类型始终是按值复制的,而其他所有内容都是浅复制。由于垃圾收集器的存在,这种浅复制很容易实现。 - Mike76
1
@getsadzeg 我所做的是使用非最终本地变量执行需要执行的任何工作,然后将任何结果复制到其他仅存在于“伪闭包”中的最终变量中。这很丑陋,但如果我在Java中担心丑陋的东西,那么我会成为一个非常沮丧的人。 - Pointy
1
@Pointy 当你想要初始化一个final field时,情况是相同的,例如 try { MY_CONSTANT = foo(); } catch(SomeException ex) { MY_CONSTANT = foo(); } 是无法工作的; 解决方案与你想要捕获的变量相同。因此,它实际上与闭包无关,只是与确切分配的严格性有关。 - Holger
显示剩余5条评论

26
匿名类中的方法实际上并没有访问局部变量和方法参数的权限。相反,当匿名类的对象被实例化时,被该对象方法所引用的局部变量和方法参数的final副本将作为实例变量存储在该对象中。匿名类对象中的方法实际上访问的是那些隐藏的实例变量。因此,局部类方法访问的局部变量和方法参数必须声明为final,以防止在实例化对象后更改它们的值。
[1] http://www.developer.com/java/other/article.php/3300881/The-Essence-of-OOP-using-Java-Anonymous-Classes.htm

1
+1 是为了明确指出限制(直到Java 8)是针对局部变量和方法参数而不是实例变量。 - sactiw

13

定义类时周围的变量存在于堆栈上,因此当内部类中的代码运行时,它们可能已经消失了(如果想知道为什么,请搜索堆栈和堆)。这就是为什么内部类实际上不使用包含方法中的变量,而是用它们的副本构建。

这意味着如果在构造内部类之后更改包含方法中的变量,则其值不会在内部类中更改,即使您希望它发生变化也不行。为了避免混淆,Java要求将它们设置为final,以便您无法修改它们。


我认为这是最好的答案。局部变量存在于堆栈上。但是类存在于堆上。因此,JVM通过其构造方法复制这些变量。但是如果变量发生更改,则这两个值是不同的。 - Charon Chui

9
Java不完全支持所谓的“闭包”,因此需要使用final,但是编译器通过生成一些隐藏变量来实现所需的功能。如果您反汇编生成的字节码,可以看到编译器的实现方式,包括奇怪命名的隐藏变量,其中包含了final变量的副本。
这是一种优雅的解决方案,可以在不弯曲语言的情况下提供功能。
编辑:对于Java 8,lambda表达式提供了更简洁的方式来执行以前使用匿名类完成的操作。变量的限制也从“final”放宽到“基本上是final” - 您不必声明它为final,但是如果它被视为final(您可以添加final关键字并且您的代码仍将编译),则可以使用它。这是一个非常好的改变。

1
我本来可能会说“不优雅”而不是“优雅”,但事实就是这样。我猜在Java8中会好一点... :) - rogerdpack
语法优雅。而且 - 像往常一样 - 随意写出更好的答案。 - Thorbjørn Ravn Andersen

7

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