首页 文章

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

提问于
浏览
316
  • 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;

        }
    });
}
  • 如何在点击时返回 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 
        }
    });
}

13 回答

  • 16

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

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

  • 2

    好吧,在Java中,变量最终不仅仅是一个参数,而是一个类级别的字段,比如

    public class Test
    {
     public final int a = 3;
    

    或者作为局部变量,如

    public static void main(String[] args)
    {
     final int a = 3;
    

    如果要从匿名类访问和修改变量,可能需要在 enclosing 类中将变量设置为 class-level 变量 .

    public class Test
    {
     public int a;
     public void doSomething()
     {
      Runnable runnable =
       new Runnable()
       {
        public void run()
        {
         System.out.println(a);
         a = a+1;
        }
       };
     }
    }
    

    你不能拥有一个变量 and 给它一个新值 . final 就是这样: Value 是不可改变的和最终的 .

    而且由于它是最终的,Java可以安全 copy 它到本地匿名类 . 你没有得到一些int的引用(特别是因为你不能在Java中引用像int这样的原语,只是引用 Objects ) .

    它只是将a的值复制到匿名类中称为a的隐式int中 .

  • -2

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

  • 5

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

  • 447

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

    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);
    
  • 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;
    }
    
  • 0

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

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

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

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

  • 2

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

    考虑将lambda代替应用的用例存储在某个地方并稍后运行 .

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

  • 41

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

    public class Program {
    
        interface Interface {
            public void printInteger();
        }
        static Interface interfaceInstance = null;
    
        static void initialize(int val) {
            class Class implements interface {
                @Override
                public void printInteger() {
                    System.out.println(val);
                }
            }
            interfaceInstance = new Class();
        }
    
        public static void main(String[] args) {
            initialize(12345);
            interfaceInstance.printInteger();
        }
    }
    

    在initialize方法返回后,interfaceInstance保留在内存中,但参数val不会 . JVM无法访问其作用域之外的局部变量,因此Java通过将val的值复制到interfaceInstance中的同名隐式字段来进行后续对printInteger的调用 . 据说interfaceInstance具有 captured 本地参数的值 . 如果参数不是最终的(或实际上是最终的),则其值可能会发生变化,与捕获的值不同步,从而可能导致不直观的行为 .

  • 10
    private void f(Button b, final int a[]) {
    
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                a[0] = a[0] * 5;
    
            }
        });
    }
    
  • 7

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

    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]);
    
    // ...
    
  • 1

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

    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的 Value 并在你想要的地方使用它 .

    Answer of your why is :

    本地内部类实例绑定到Main类,并且可以访问其包含方法的最终局部变量 . 当实例使用其包含方法的最终局部时,该变量将保留实例创建时保留的值,即使该变量已超出范围(这实际上是Java的粗略,有限版本的闭包) .

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

  • 0

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


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

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

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

    使变量最终消除所有这些可能性 - 因为值可以't be changed at all, you don'需要担心这些变化是否可见 . 允许方法和匿名内部类看到彼此的唯一方法's changes is to use a mutable type of some description. This could be the enclosing class itself, an array, a mutable wrapper type... anything like that. Basically it'有点像在一个方法和另一个方法之间进行通信:对一个方法的参数所做的更改不会被其调用者看到,而是对所引用的对象所做的更改由参数可见 .

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

相关问题