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

时间:2011-01-19 06:58:42

标签: java event-handling anonymous-class

  1. 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;
    
            }
        });
    }
    
  2. 如何在点击时返回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 个答案:

答案 0 :(得分:462)

正如评论中所指出的,其中一些在Java 8中变得无关紧要,其中final可能是隐含的。但是,只有有效最终变量可用于匿名内部类或lambda表达式。


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

当您创建匿名内部类的实例时,该类中使用的任何变量都会通过自动生成的构造函数复制其。这避免了编译器必须自动生成各种额外类型以保持“局部变量”的逻辑状态,例如C#编译器确实...(当C#捕获匿名函数中的变量时,它实际捕获变量 - 闭包可以以方法主体看到的方式更新变量,反之亦然。)

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

使变量最终删除所有这些可能性 - 因为根本无法更改值,您不必担心这些更改是否可见。允许方法和匿名内部类看到彼此的更改的唯一方法是使用可变类型的某些描述。这可能是封闭类本身,一个数组,一个可变的包装类型......类似的东西。基本上它有点像在一个方法和另一个方法之间进行通信:对一个方法的参数所做的更改不会被其调用者看到,但对对象所做的更改将被引用到通过参数可以看出。

如果您对Java和C#闭包之间更详细的比较感兴趣,我有article进一步深入研究。我想在这个答案中关注Java方面:)

答案 1 :(得分:42)

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

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?
}

然而,由于同步问题,这个技巧不是很好。如果稍后调用handler,则需要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]);

// ...

答案 2 :(得分:16)

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

  

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

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

Jon has the complete answer - 我保留此未删除的内容,因为人们可能对JLS规则感兴趣)

答案 3 :(得分:10)

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

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类绑定,可以访问其包含方法的最终局部变量。当实例使用其包含方法的最终局部时,该变量将保留在创建实例时保留的值,即使该变量已超出范围(这实际上是Java的粗略,有限版本的闭包)。 / p>

因为本地内部类既不是类或包的成员,也不会使用访问级别声明它。 (但要明确的是,它自己的成员具有像普通类一样的访问级别。)

答案 4 :(得分:6)

嗯,在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中。

答案 5 :(得分:6)

访问仅限于本地最终变量的原因是,如果所有局部变量都可以访问,那么首先需要将它们复制到一个单独的部分,其中内部类可以访问它们并维护可变局部变量的多个副本可能导致数据不一致。而最终变量是不可变的,因此任何数量的副本都不会对数据的一致性产生任何影响。

答案 6 :(得分:2)

在生成它的线程终止之后,可以很好地调用非自治内部类中的方法。在您的示例中,将在事件调度线程上调用内部类,而不是在创建它的线程中调用内部类。因此,变量的范围将是不同的。因此,为了保护这些变量赋值范围问题,您必须将它们声明为final。

答案 7 :(得分:2)

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

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

答案 8 :(得分:1)

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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

答案 9 :(得分:1)

要了解此限制的原理,请考虑以下程序:

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 initialize 方法返回后仍保留在内存中,但是参数 val 没有。 JVM无法访问其范围之外的局部变量,因此Java通过将 val 的值复制到同名的隐式字段中来使对 printInteger 的后续调用有效在 interfaceInstance 中。据说 interfaceInstance 捕获了本地参数的值。如果该参数不是最终参数(或实际上不是最终参数),则其值可能会更改,从而与捕获的值不同步,从而可能导致不直观的行为。

答案 10 :(得分:0)

由于Jon具有实现细节,因此另一个可能的答案是JVM不想处理已经结束激活的写入记录。

考虑使用lambda而不是应用的用例,将其存储在某个地方并稍后运行。

我记得在Smalltalk中,当你进行这样的修改时,你会得到一个非法商店。

答案 11 :(得分:0)

尝试此代码,

创建数组列表并将值放入其中并返回:

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

答案 12 :(得分:0)

Java匿名类非常类似于Javascript闭包,但是Java以不同的方式实现它。 (检查安徒生的答案)

因此,为了不使Java Developer与那些来自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决定强制将其更改为最终版本,因此数据从始至终都是“一致的”。

答案 13 :(得分:0)

内部类中的Java最终变量

内部类只能使用

  1. 外部类的引用
  2. 最终超出范围的局部变量是引用类型(例如Object ...)
  3. 值(原始)(例如int ...)类型可以由最终引用类型包装IntelliJ IDEA可以帮助您将其隐藏到一个元素 array

当编译器生成non static nestedinner class[About]时-将创建一个新类-<OuterClass>$<InnerClass>.class并将绑定参数传递给构造函数 strong> [Local variable on stack]。它类似于闭包

最终变量是不能重新分配的变量。最终参考变量仍然可以通过修改状态来更改

这很可能很奇怪,因为作为程序员,您可以像这样

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}

答案 14 :(得分:-2)

也许这个技巧给了你一个想法

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);
相关问题