【问题标题】:Why are only final variables accessible in anonymous class?为什么在匿名类中只能访问最终变量?
【发布时间】:2011-06-11 13:53:02
【问题描述】:
  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. 点击后如何返回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 
            }
        });
    }
    

【问题讨论】:

  • 我不认为 Java 匿名类提供了你所期望的那种 lambda 闭包,但如果我错了,请有人纠正我......
  • 你想达到什么目的?当“f”完成时,可以执行点击处理程序。
  • @Lambert 如果你想在 onClick 方法中使用 a 它必须是最终的
  • 这就是我的意思——它不支持完全闭包,因为它不允许访问非最终变量。
  • 注意:从 Java 8 开始,您的变量只需实际上是最终的

标签: java event-handling anonymous-class


【解决方案1】:

正如 cmets 中所述,其中一些在 Java 8 中变得无关紧要,其中 final 可以是隐式的。但是,只有 有效 final 变量可以在匿名内部类或 lambda 表达式中使用。


这基本上是由于Java管理closures的方式。

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

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

将变量设为 final 消除了所有这些可能性 - 由于根本无法更改值,因此您无需担心此类更改是否可见。允许方法和匿名内部类看到彼此更改的唯一方法是使用某种描述的可变类型。这可能是封闭类本身、一个数组、一个可变包装器类型......任何类似的东西。基本上,这有点像一种方法和另一种方法之间的通信:调用者看不到对一种方法的参数所做的更改,但对引用的对象所做的更改由参数可见。

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

【讨论】:

  • 是的。基本上,完全的闭包支持可以通过移动一个特殊的自动生成类中引用的所有变量来实现。
  • @Ivan:基本上就像 C#。但是,如果您想要与 C# 相同的功能,其中来自不同范围的变量可以被“实例化”不同次数,那么它具有相当程度的复杂性。
  • 这对 Java 7 来说都是正确的,请记住在 Java 8 中引入了闭包,现在确实可以从内部类访问类的非最终字段。
  • @MathiasBader:真的吗?我认为它本质上仍然是相同的机制,编译器现在足够聪明地推断出final(但它仍然需要有效地最终)。
  • @Mathias Bader:您始终可以访问非最终的字段,不要与必须是最终的local变量混淆并且仍然必须是有效的 final,因此 Java 8 不会改变语义。
【解决方案2】:

有一个技巧可以让匿名类在外部范围内更新数据。

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

// ...

【讨论】:

  • 感谢您的回答。我知道这一切,我的解决方案比这更好。我的问题是“为什么只有决赛”?
  • 那么答案就是它们的实现方式:)
  • 谢谢。我自己使用了上面的技巧。我不确定这是否是个好主意。如果 Java 不允许这样做,可能有充分的理由。您的回答表明我的List.forEach 代码是安全的。
  • 阅读stackoverflow.com/q/12830611/2073130,了解“为什么只有最终”背后的基本原理。
  • 有几种解决方法。我的是:final int resf = res;最初我使用数组方法,但我发现它的语法过于繁琐。 AtomicReference 可能会慢一些(分配一个对象)。
【解决方案3】:

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

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

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

Jon has the complete answer - 我保留这个不删除,因为有人可能对 JLS 规则感兴趣)

【讨论】:

    【解决方案4】:

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

    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 的粗略、受限版本的闭包)。

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

    【讨论】:

    • 我提到“不保留它作为私人成员”
    • 先生,您能否简单地说一下“即使变量超出范围”是什么意思?先生。
    【解决方案5】:

    嗯,在 Java 中,一个变量不仅可以作为参数,还可以作为类级别的字段,比如

    public class Test
    {
     public final int a = 3;
    

    或作为局部变量,如

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

    如果您想访问和修改来自匿名类的变量,您可能需要将该变量设为封闭类中的类级变量。 p>

    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 的意思就是:该值是不可更改的和最终的。

    由于它是最终的,Java 可以安全地将其复制到本地匿名类。您没有获得对 int 的一些 reference(尤其是因为您不能引用 Java 中的 int 等原语,只能引用 Objects)。

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

    【讨论】:

    • 我将“类级变量”与static 相关联。如果你改用“实例变量”可能会更清楚。
    • 好吧,我使用了类级别,因为该技术适用于实例变量和静态变量。
    • 我们已经知道 final 是可访问的,但我们想知道为什么?您能否在为什么方面添加更多解释?
    【解决方案6】:

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

    【讨论】:

    • 这不是在支持此功能的 C# 等语言中实现的方式。实际上,编译器将变量从局部变量更改为实例变量,或者它为这些变量创建了一个额外的数据结构,可以超出外部类的范围。但是,没有“局部变量的多个副本”
    • Mike76 我没有看过 C# 的实现,但 Scala 做了你提到的第二件事,我认为:如果 Int 被重新分配到闭包内,则将该变量更改为实例IntRef (本质上是一个可变的 Integer 包装器)。然后相应地重写每个变量访问。
    【解决方案7】:

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

    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 的值复制到同名的隐式字段来使随后对 printInteger 的调用工作在 interfaceInstance 中。 interfaceInstance 被称为 捕获 本地参数的值。如果参数不是最终的(或实际上是最终的),它的值可能会发生变化,与捕获的值不同步,从而可能导致不直观的行为。

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

        异常内部类中的方法可能会在产生它的线程终止后被调用。在您的示例中,内部类将在事件分派线程上调用,而不是在与创建它的线程相同的线程中。因此,变量的范围会有所不同。因此,为了保护此类变量分配范围问题,您必须将它们声明为 final。

        【讨论】:

          【解决方案10】:
          private void f(Button b, final int a[]) {
          
              b.addClickHandler(new ClickHandler() {
          
                  @Override
                  public void onClick(ClickEvent event) {
                      a[0] = a[0] * 5;
          
                  }
              });
          }
          

          【讨论】:

            【解决方案11】:

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

            考虑一个用例,您的 lambdas 不是被应用,而是存储在某个地方并稍后运行。

            我记得在 Smalltalk 中,当您进行此类修改时,您会遇到一个非法商店。

            【讨论】:

              【解决方案12】:

              试试这个代码,

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

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

              【讨论】:

              • OP 正在询问为什么需要某些东西的原因。因此,您应该指出您的代码是如何解决它的
              【解决方案13】:

              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 决定强制它是最终的,所以数据从头到尾都是“一致的”。

              【讨论】:

                【解决方案14】:

                inner class[About] 中的 Java final 变量

                内部类只能使用

                1. 来自外部类的引用
                2. 最终局部变量超出范围,属于引用类型(例如Object...)
                3. value(primitive)(例如int...)类型可以由最终引用类型包装IntelliJ IDEA 可以帮你把它转换成一个元素array

                当编译器生成non static nested (inner class) 时 - 创建一个新类 - <OuterClass>$<InnerClass>.class 并将 bounded 参数传递到 constructor@987654322 @类似于闭包[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
                }
                

                【讨论】:

                  【解决方案15】:

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

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

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2019-03-16
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多