a
在这里只能是最终的。为什么?如何a
在onClick()
不将其保留为私有成员的情况下重新分配方法?private void f(Button b, final int a){ b.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { int b = a*5; } }); }
单击时如何返回
5 * a
?我是说,private void f(Button b, final int a){ b.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { int b = a*5; return b; // but return type is void } }); }
15 回答
正如评论中所指出的,其中一些在 Java 8 中变得无关紧要,其中final
可能是隐式的。但是,只有一个有效的最终变量才能在匿名内部类或 lambda 表达式中使用。
这基本上是由于 Java 管理闭包的方式。
当您创建匿名内部类的实例时,该类中使用的任何变量都会通过自动生成的构造函数复制其值。这避免了编译器必须自动生成各种额外的类型来保存“局部变量”的逻辑状态,例如 C# 编译器......(当 C# 在匿名函数中捕获变量时,它确实捕获了变量 -闭包可以以方法主体看到的方式更新变量,反之亦然。)
由于该值已被复制到匿名内部类的实例中,因此如果该变量可以被该方法的其余部分修改,那将看起来很奇怪——您可能拥有似乎正在使用过期变量的代码(因为这实际上就是会发生的事情……您将使用在不同时间拍摄的副本)。同样,如果您可以在匿名内部类中进行更改,开发人员可能希望这些更改在封闭方法的主体中可见。
将变量设为 final 消除了所有这些可能性——因为值根本无法更改,您无需担心此类更改是否可见。允许方法和匿名内部类看到彼此更改的唯一方法是使用某种描述的可变类型。这可能是封闭类本身、一个数组、一个可变包装器类型......任何类似的东西。基本上,这有点像一种方法和另一种方法之间的通信:调用者看不到对一种方法的参数所做的更改,但可以看到对参数引用的对象所做的更改。
如果您对 Java 和 C# 闭包之间的更详细比较感兴趣,我有一篇文章会进一步讨论。我想在这个答案中专注于 Java 方面:)
有一个技巧可以让匿名类在外部范围内更新数据。
private void f(Button b, final int a) {
final int[] res = new int[1];
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
res[0] = a * 5;
}
});
// But at this point handler is most likely not executed yet!
// How should we now res[0] is ready?
}
但是,由于同步问题,这个技巧不是很好。如果稍后调用处理程序,则需要 1) 如果从不同线程调用处理程序,则同步对 res 的访问 2) 需要具有某种标志或指示 res 已更新
但是,如果立即在同一线程中调用匿名类,则此技巧可以正常工作。像:
// ...
final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);
// ...
匿名类是内部类,严格规则适用于内部类 (JLS 8.1.3):
任何使用但未在内部类中声明的局部变量、形式方法参数或异常处理程序参数都必须声明为 final。任何在内部类中使用但未声明的局部变量必须在内部类的主体之前明确分配。
我还没有找到关于 jls 或 jvms 的原因或解释,但我们知道,编译器为每个内部类创建一个单独的类文件,它必须确保在这个类文件上声明的方法(在字节码级别)至少可以访问局部变量的值。
(乔恩有完整的答案- 我保持这个不删除,因为有人可能对 JLS 规则感兴趣)
您可以创建一个类级别的变量来获取返回值。我是说
class A {
int k = 0;
private void f(Button b, int a){
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
k = a * 5;
}
});
}
现在你可以得到 K 的值并在你想要的地方使用它。
你为什么的答案是:
本地内部类实例与 Main 类绑定,并且可以访问其包含方法的最终局部变量。当实例使用其包含方法的 final 局部变量时,该变量将保留它在实例创建时所持有的值,即使该变量已经超出范围(这实际上是 Java 的粗略、有限的闭包版本)。
因为本地内部类既不是类的成员也不是包的成员,所以它没有用访问级别声明。(但是要清楚,它自己的成员具有与普通类一样的访问级别。)
要了解此限制的基本原理,请考虑以下程序:
public class Program {
interface Interface {
public void printInteger();
}
static Interface interfaceInstance = null;
static void initialize(int val) {
class Impl implements Interface {
@Override
public void printInteger() {
System.out.println(val);
}
}
interfaceInstance = new Impl();
}
public static void main(String[] args) {
initialize(12345);
interfaceInstance.printInteger();
}
}
在initialize方法返回后interfaceInstance保留在内存中,但参数val没有。JVM 无法访问其范围之外的局部变量,因此 Java通过将val的值复制到interfaceInstance中同名的隐式字段来使后续调用printInteger工作。据说interfaceInstance已经捕获了本地参数的值。如果参数不是最终的(或实际上是最终的),它的值可能会改变,与捕获的值不同步,可能会导致不直观的行为。
好吧,在 Java 中,一个变量不仅可以作为参数,还可以作为类级别的字段,例如
public class Test
{
public final int a = 3;
或作为局部变量,例如
public static void main(String[] args)
{
final int a = 3;
如果要访问和修改来自匿名类的变量,您可能希望使该变量成为封闭类中的类级别变量。
public class Test
{
public int a;
public void doSomething()
{
Runnable runnable =
new Runnable()
{
public void run()
{
System.out.println(a);
a = a+1;
}
};
}
}
您不能将变量作为最终变量并为其赋予新值。final
只是意味着:价值是不可改变的和最终的。
而且由于它是最终的,Java 可以安全地将其复制到本地匿名类中。您没有获得对 int 的一些引用(特别是因为您不能引用 Java 中的 int 等原语,而只能引用Objects)。
它只是将 a 的值复制到匿名类中称为 a 的隐式 int 中。
访问仅限于局部最终变量的原因是,如果所有局部变量都可以访问,那么首先需要将它们复制到内部类可以访问它们并维护多个副本的单独部分可变局部变量可能导致数据不一致。而最终变量是不可变的,因此对它们的任何数量的副本都不会对数据的一致性产生任何影响。
当在方法体中定义匿名内部类时,在该方法范围内声明为 final 的所有变量都可以从内部类中访问。对于标量值,一旦被赋值,最终变量的值就不能改变。对于对象值,引用不能更改。这允许 Java 编译器在运行时“捕获”变量的值并将副本作为字段存储在内部类中。一旦外部方法终止并且它的堆栈帧被删除,原始变量就消失了,但内部类的私有副本仍然存在于类自己的内存中。
异常内部类中的方法可能会在产生它的线程终止后被调用。在您的示例中,内部类将在事件分派线程上调用,而不是在与创建它的线程相同的线程中。因此,变量的范围会有所不同。因此,为了保护此类变量分配范围问题,您必须将它们声明为 final。
private void f(Button b, final int a[]) {
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
a[0] = a[0] * 5;
}
});
}
由于Jon有实现细节的答案,另一个可能的答案是 JVM 不想处理已经结束他的激活的写入记录。
考虑将您的 lambdas 存储在某个地方并稍后运行的用例,而不是被应用。
我记得在 Smalltalk 中,当您进行此类修改时,您会遇到非法商店。
试试这个代码,
创建数组列表并将值放入其中并返回:
private ArrayList f(Button b, final int a)
{
final ArrayList al = new ArrayList();
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
int b = a*5;
al.add(b);
}
});
return al;
}
Java 匿名类与 Javascript 闭包非常相似,但 Java 以不同的方式实现它。(检查安徒生的答案)
因此,为了不让 Java 开发人员与来自 Javascript 背景的人可能发生的奇怪行为混淆。我想这就是他们强迫我们使用的原因final
,这不是 JVM 的限制。
让我们看一下下面的 Javascript 示例:
var add = (function () {
var counter = 0;
var func = function () {
console.log("counter now = " + counter);
counter += 1;
};
counter = 100; // line 1, this one need to be final in Java
return func;
})();
add(); // this will print out 100 in Javascript but 0 in Java
在 Javascript 中,该值将是 100,因为从头到尾counter
只有一个变量。counter
但是在 Java 中,如果没有final
,它会打印出0
,因为在创建内部对象时,该0
值被复制到内部类对象的隐藏属性中。(这里有两个整数变量,一个在本地方法中,另一个在内部类隐藏属性中)
因此内部对象创建后的任何更改(如第 1 行)都不会影响内部对象。因此,它会混淆两种不同的结果和行为(Java 和 Javascript 之间)。
我相信这就是为什么 Java 决定强制它是最终的,所以数据从头到尾都是“一致的”。
[关于]中的Javafinal
变量inner class
内部类只能使用
- 外部类的引用
- 超出范围的最终局部变量是引用类型(例如
Object
...) - value(primitive) (eg
int
...) 类型可以被最终引用类型包装。IntelliJ IDEA
可以帮助您将其转换为一个元素数组
当编译器生成一个non static nested
( inner class
)——一个新的类——<OuterClass>$<InnerClass>.class
被创建并且有界参数被传递给构造函数[栈上的局部变量]它类似于闭包[Swift about]
final 变量是一个不能重新赋值的变量。最终引用变量仍然可以通过修改状态来更改
如果有可能,那会很奇怪,因为作为程序员,您可以像这样
//Not possible
private void foo() {
MyClass myClass = new MyClass(); //Case 1: myClass address is 1
int a = 5; //Case 2: a = 5
//just as an example
new Button().addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
/*
myClass.something(); //<- what is the address - 1 or 2?
int b = a; //<- what is the value - 5 or 10 ?
//illusion that next changes are visible for Outer class
myClass = new MyClass();
a = 15;
*/
}
});
myClass = new MyClass(); //Case 1: myClass address is 2
int a = 10; //Case 2: a = 10
}
也许这个技巧给了你一个想法
Boolean var= new anonymousClass(){
private String myVar; //String for example
@Overriden public Boolean method(int i){
//use myVar and i
}
public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);