为什么在匿名类中只能访问最终变量?

  1. a在这里只能是最终版本。为什么?如何在 onClick()方法中重新分配 a而不保留它作为私有成员?

    private void f(Button b, final int a){
    b.addClickHandler(new ClickHandler() {
    
    
    @Override
    public void onClick(ClickEvent event) {
    int b = a*5;
    
    
    }
    });
    }
    
  2. How can I return the 5 * a when it clicked? I mean,

    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
    }
    });
    }
    
126417 次浏览

有一个技巧允许匿名类在外部作用域中更新数据。

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]);


// ...

您可以创建一个类级别的变量来获取返回值。我的意思是

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中粗糙的、有限版本的闭包),变量也会保留它在创建实例时所持有的值。

因为局部内部类既不是类的成员也不是包的成员,所以没有使用访问级别声明它。(但是要清楚的是,它自己的成员像普通类一样具有访问级别。)

正如注释中所指出的,其中一些在Java 8中变得无关紧要,在Java 8中final可以是隐式的。不过,只有有效地 final变量可以用于匿名内部类或lambda表达式。


这主要是由于Java管理闭包的方式。

当创建匿名内部类的实例时,该类中使用的任何变量都会通过自动生成的构造函数复制它们的。这就避免了编译器必须自动生成各种额外的类型来保存“局部变量”的逻辑状态,就像c#编译器所做的那样……(当c#在匿名函数中捕获变量时,它实际上捕获了变量——闭包可以以方法主体可以看到的方式更新变量,反之亦然。)

由于该值已被复制到匿名内部类的实例中,如果该变量可以由方法的其余部分修改,那么看起来会很奇怪——你可能会有代码似乎正在使用一个过时的变量(因为这实际上就是正在发生的事情……你将使用在不同时间拍摄的副本)。同样,如果您可以在匿名内部类中进行更改,开发人员可能希望这些更改在封闭方法的主体中可见。

将变量设为final可以消除所有这些可能性——因为值根本无法更改,所以不需要担心这些更改是否可见。允许方法和匿名内部类看到彼此更改的唯一方法是使用某种描述的可变类型。这可以是外围类本身、数组、可变包装器类型……诸如此类。基本上,这有点像一个方法和另一个方法之间的通信:对一个方法的参数所做的更改不会被它的调用者看到,但参数对对象所做的更改会被看到。

如果你对Java和c#闭包之间更详细的比较感兴趣,我有一个文章,它会进一步深入。在这个答案中,我想专注于Java方面:)

匿名内部类中的方法可以在生成该类的线程终止后调用。在您的示例中,内部类将在事件分派线程上调用,而不是在创建它的线程中调用。因此,变量的作用域是不同的。所以为了保护这样的变量赋值范围问题,你必须声明它们为final。

在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 而且给它一个新值。final的意思是:这个值是不可改变的,是final的。

由于它是最终的,Java可以安全地复制它到本地匿名类。你不会得到一些参考到int(特别是因为你不能在Java中引用像int这样的原语,只能引用对象)。

它只是将a的值复制到一个隐式int,在匿名类中称为a。

匿名类是内部类,严格规则适用于内部类 (JLS 8.1.3):

任何在内部类必须声明为最终结果中使用但未声明的局部变量、形式方法参数或异常处理程序参数。任何局部变量,在内部类必须在内部类的主体之前确定分配中使用但未声明。

我还没有在jls或jvm上找到一个原因或解释,但我们知道,编译器为每个内部类创建了一个单独的类文件,它必须确保,在这个类文件上声明的方法(在字节码级别上)至少可以访问局部变量的值。

(乔恩有完整的答案 -我保持这个未删除,因为有人可能对JLS规则感兴趣)

private void f(Button b, final int a[]) {


b.addClickHandler(new ClickHandler() {


@Override
public void onClick(ClickEvent event) {
a[0] = a[0] * 5;


}
});
}

也许这个把戏能给你启发

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);

访问被限制为局部final变量的原因是,如果所有的局部变量都是可访问的,那么它们首先需要被复制到一个单独的部分,在那里内部类可以访问它们,并且维护可变局部变量的多个副本可能会导致数据不一致。而final变量是不可变的,因此任何数量的拷贝都不会对数据的一致性产生任何影响。

当在方法体中定义匿名内部类时,所有在该方法作用域中声明为final的变量都可以从内部类中访问。对于标量值,一旦分配了它,最终变量的值就不能更改。对于对象值,引用不能更改。这允许Java编译器在运行时“捕获”变量的值,并将其副本作为内部类中的字段存储。一旦外部方法终止,其堆栈框架被移除,原始变量就会消失,但内部类的私有副本会保留在类自己的内存中。

(http://en.wikipedia.org/wiki/Final_%28Java%29)

由于乔恩有实现细节答案,另一个可能的答案是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;
}

要理解这种限制的基本原理,请考虑以下程序:

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();
}
}

interfaceInstance初始化方法返回后仍保留在内存中,但参数瓦尔不会。JVM不能在其作用域之外访问局部变量,因此Java通过将瓦尔的值复制到interfaceInstance内同名的隐式字段来实现对printInteger的后续调用。interfaceInstance被认为具有捕获本地参数的值。如果参数不是最终值(或有效的最终值),它的值可能会改变,与捕获的值不同步,可能会导致不直观的行为。

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中,counter值将是100,因为从头到尾只有一个counter变量。

但在Java中,如果没有final,它将打印出0,因为在创建内部对象时,0值将被复制到内部类对象的隐藏属性中。(这里有两个整数变量,一个在局部方法中,另一个在内部类隐藏属性中)

因此,内部对象创建后的任何更改(如第1行)都不会影响内部对象。所以它会混淆两种不同的结果和行为(Java和Javascript之间)。

我相信这就是为什么,Java决定强制它是最终的,所以数据从开始到结束都是“一致的”。

Java final变量内的inner class<一口>[对]< /一口>

内部类只能使用

  1. 外部类引用
  2. final局部变量来自范围外的引用类型(例如Object…)
  3. value(基元)(例如int…)类型可以通过最终引用类型包装IntelliJ IDEA可以帮助你将它转换为一个元素数组

当编译器生成一个non static nested (inner class)时——一个新类——<OuterClass>$<InnerClass>.class被创建,有界的参数被传递给构造函数[堆栈上的本地变量] . [/sup> .。它类似于closure<一口>(斯威夫特对)< /一口>

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
}