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

341 java event-handling anonymous-class

  1. a只能在这里决赛.为什么?如何aonClick()不将其保留为私有成员的情况下重新分配方法?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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 
            }
        });
    }
    
    Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 477

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


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

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

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

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

如果您对Java和C#闭包之间更详细的比较感兴趣,我有一篇文章进一步介绍它.我想在这个答案中专注于Java方面:)

  • @MathiasBader:真的吗?我认为它仍然基本上是相同的机制,编译器现在只是聪明到足以推断`final`(但它仍然需要有效的最终). (21认同)
  • 这对Java 7来说都是如此,请记住,使用Java 8时,已经引入了闭包,现在确实可以从其内部类访问类的非final字段. (11认同)
  • @Ivan:基本上就像C#一样.它具有相当程度的复杂性,但是如果你想要与C#相同的功能,来自不同范围的变量可以被"实例化"不同的次数. (4认同)
  • http://stackoverflow.com/questions/7472195/passing-final-variables-to-anonymous-classes (3认同)
  • @Mathias Bader:你总是可以访问非final*字段*,这些字段不要与*local*变量混淆,后者必须是最终的,但仍然必须是最终的,所以Java 8不会改变语义. (3认同)
  • 是啊.基本上,可以通过移动在特殊自动生成的类中引用的所有变量来实现完全闭包支持. (2认同)
  • 如果有人像我一样愚蠢,并且如果您撞墙撞头只是想知道这种关闭是什么,那么...那么您就去了。.https://www.youtube.com/watch?v=Nj3_DMUXEbE (2认同)
  • @lupchiazoem:假设您从一个方法返回一个匿名内部类实例。局部变量在堆栈上——它们现在在哪里?您希望实例如何引用它们?恐怕“C# 编译器对委托做了什么”这个话题太大了,无法在评论中详细介绍,同样“Java 编译器对匿名内部类做了什么”。 (2认同)

Iva*_*rov 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?
}
Run Code Online (Sandbox Code Playgroud)

但是,由于同步问题,这个技巧不是很好.如果稍后调用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]);

// ...
Run Code Online (Sandbox Code Playgroud)

  • 那么答案就是它们是如何实现的:) (6认同)
  • 您也可以使用例如"AtomicReference"或"AtomicInt"来达到同样的目的...... (3认同)
  • 感谢您的回答.我知道所有这一切,我的解决方案比这更好.我的问题是"为什么只有最终"? (2认同)

And*_*s_D 17

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

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

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

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


Ash*_*och 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;
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

现在你可以获得K的价值并在你想要的地方使用它.

回答你的原因是:

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

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


Zac*_*h L 6

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

public class Test
{
 public final int a = 3;
Run Code Online (Sandbox Code Playgroud)

或者作为局部变量,如

public static void main(String[] args)
{
 final int a = 3;
Run Code Online (Sandbox Code Playgroud)

如果要从匿名类访问和修改变量,可能需要在封闭类中将变量设置为类级变量.

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

您不能将变量作为final,为其赋予新值.final意思是:价值是不可改变的和最终的.

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

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

  • 我将"class-level variable"与`static`联系起来.也许更清楚的是你是否使用"实例变量". (3认同)

Swa*_*ade 6

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


小智 6

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

interfaceInstance保留在内存后初始化方法的返回值,但参数VAL没有。JVM 无法访问其范围之外的局部变量,因此 Java通过将val的值复制到interfaceInstance中的同名隐式字段来使对printInteger的后续调用起作用。该interfaceInstance据说已抓获本地参数的值。如果参数不是最终的(或实际上是最终的),它的值可能会改变,与捕获的值不同步,可能会导致不直观的行为。


归档时间:

查看次数:

108331 次

最近记录:

6 年,12 月 前