【问题标题】:Cannot refer to a non-final variable inside an inner class defined in a different method不能在不同方法中定义的内部类中引用非最终变量
【发布时间】:2010-11-20 22:52:57
【问题描述】:

编辑: 我需要更改几个变量的值,因为它们通过计时器运行了几次。我需要通过计时器在每次迭代中不断更新值。我无法将值设置为最终值,因为这将阻止我更新值,但是我收到了我在下面最初问题中描述的错误:

我之前写过以下内容:

我收到错误“无法引用以不同方法定义的内部类中的非最终变量”。

这发生在名为 price 的 double 和名为 priceObject 的 Price 上。你知道我为什么会遇到这个问题吗?我不明白为什么我需要有一个最终声明。此外,如果您能看到我正在尝试做什么,我必须做些什么来解决这个问题。

public static void main(String args[]) {

    int period = 2000;
    int delay = 2000;

    double lastPrice = 0;
    Price priceObject = new Price();
    double price = 0;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);
}

【问题讨论】:

  • 我要问的是,如何在可以不断更新的计时器中获取变量。
  • @Ankur:简单的答案是“不”。但是您可以使用内部类来达到预期的效果;请参阅@petercardona 的回答。

标签: java methods declaration final


【解决方案1】:

Java 不支持真正的closures,尽管使用像您在这里使用的匿名类 (new TimerTask() { ... }) 看起来像是一种闭包。

edit - 请参阅下面的 cmets - 正如 KeeperOfTheSoul 指出的那样,以下不是正确的解释。

这就是它不起作用的原因:

变量lastPrice 和price 是main() 方法中的局部变量。您使用匿名类创建的对象可能会持续到 main() 方法返回之后。

main()方法返回时,局部变量(如lastPriceprice)将从堆栈中清除,所以在main()返回后它们将不再存在。

但是匿名类对象引用了这些变量。如果匿名类对象在清理完变量后尝试访问这些变量,事情就会变得非常糟糕。

通过使lastPriceprice final,它们不再是真正的变量,而是常量。然后,编译器可以将匿名类中lastPriceprice 的使用替换为常量的值(当然是在编译时),并且您将不再有访问不存在的变量的问题.

其他支持闭包的编程语言通过特殊处理这些变量来做到这一点 - 通过确保它们在方法结束时不会被破坏,以便闭包仍然可以访问变量。

@Ankur:你可以这样做:

public static void main(String args[]) {
    int period = 2000;
    int delay = 2000;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        // Variables as member variables instead of local variables in main()
        private double lastPrice = 0;
        private Price priceObject = new Price();
        private double price = 0;

        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);      
}

【讨论】:

  • 不太正确,Java 确实为相关变量生成捕获以捕获它们的运行时值,只是他们想避免在.Net 中可能出现的奇怪副作用,您可以在其中捕获委托中的值,更改外部方法中的值,现在委托看到新值,请参阅stackoverflow.com/questions/271440/c-captured-variable-in-loop,这是 Java 旨在避免的这种行为的 C# 示例。
  • 这不是“奇怪的副作用”,这是人们所期望的正常行为——Java 无法提供这种行为因为它不会生成捕获。作为一种解决方法,匿名类中使用的局部变量必须是最终变量。
  • Jesper,您可能应该编辑掉您的答案中不正确的部分,而不是只看到一条消息说上述内容不正确。
  • Java 实际上不支持闭包。支持闭包的语言通过将整个本地环境(即当前堆栈帧中定义的局部变量集)存储为堆对象来实现这一点。 Java 不支持这一点(语言设计者想要实现它但时间不够用了),因此作为一种解决方法,每当实例化本地类时,它所引用的任何局部变量的值都会被复制到堆上.但是,JVM 无法使这些值与局部变量保持同步,这就是它们必须是 final 的原因。
  • 这个答案完全令人困惑,因为没有名为“KeeperOfTheSoul”的人对其发表评论。答案应该修改。
【解决方案2】:

为了避免匿名委托引用的 java 变量中的闭包产生奇怪的副作用,必须将其标记为 final,因此要在计时器任务中引用 lastPrice 和 price,它们需要标记为 final。

这显然对您不起作用,因为您希望更改它们,在这种情况下,您应该考虑将它们封装在一个类中。

public class Foo {
    private PriceObject priceObject;
    private double lastPrice;
    private double price;

    public Foo(PriceObject priceObject) {
        this.priceObject = priceObject;
    }

    public void tick() {
        price = priceObject.getNextPrice(lastPrice);
        lastPrice = price;
    }
}

现在只需创建一个新的 Foo 作为 final 并从计时器调用 .tick。

public static void main(String args[]){
    int period = 2000;
    int delay = 2000;

    Price priceObject = new Price();
    final Foo foo = new Foo(priceObject);

    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            foo.tick();
        }
    }, delay, period);
}

【讨论】:

  • 或者你可以直接让 Foo 实现 Runnable..?
【解决方案3】:

当使用匿名类时,您只能从包含类访问最终变量。因此,您需要将要使用的变量声明为 final(这不是您的选择,因为您要更改 lastPriceprice),或者不要使用匿名类。

所以你的选择是创建一个实际的内部类,你可以在其中传递变量并以正常方式使用它们

或:

您的 lastPriceprice 变量有一个快速(在我看来很丑陋)的破解方法,可以像这样声明它

final double lastPrice[1];
final double price[1];

在您的匿名类中,您可以像这样设置值

price[0] = priceObject.getNextPrice(lastPrice[0]);
System.out.println();
lastPrice[0] = price[0];

【讨论】:

    【解决方案4】:

    很好地解释了为什么你不能做你想做的事情已经提供了。作为一种解决方案,也许可以考虑:

    public class foo
    {
        static class priceInfo
        {
            public double lastPrice = 0;
            public double price = 0;
            public Price priceObject = new Price ();
        }
    
        public static void main ( String args[] )
        {
    
            int period = 2000;
            int delay = 2000;
    
            final priceInfo pi = new priceInfo ();
            Timer timer = new Timer ();
    
            timer.scheduleAtFixedRate ( new TimerTask ()
            {
                public void run ()
                {
                    pi.price = pi.priceObject.getNextPrice ( pi.lastPrice );
                    System.out.println ();
                    pi.lastPrice = pi.price;
    
                }
            }, delay, period );
        }
    }
    

    似乎您可以做一个比这更好的设计,但想法是您可以将更新的变量分组到一个不变的类引用中。

    【讨论】:

      【解决方案5】:

      对于匿名类,您实际上是在声明一个“无名”嵌套类。对于嵌套类,编译器生成一个带有构造函数的新的独立公共类,该构造函数将它使用的所有变量作为参数(对于“命名”嵌套类,这始终是原始/封闭类的实例)。这样做是因为运行时环境没有嵌套类的概念,因此需要从嵌套类(自动)转换为独立类。

      以这段代码为例:

      public class EnclosingClass {
          public void someMethod() {
              String shared = "hello"; 
              new Thread() {
                  public void run() {
                      // this is not valid, won't compile
                      System.out.println(shared); // this instance expects shared to point to the reference where the String object "hello" lives in heap
                  }
              }.start();
      
              // change the reference 'shared' points to, with a new value
              shared = "other hello"; 
              System.out.println(shared);
          }
      }
      

      这是行不通的,因为这是编译器在后台所做的:

      public void someMethod() {
          String shared = "hello"; 
          new EnclosingClass$1(shared).start();
      
          // change the reference 'shared' points to, with a new value
          shared = "other hello"; 
          System.out.println(shared);
      }
      

      原来的匿名类被编译器生成的一些独立类替换(代码不准确,但应该给你一个好主意):

      public class EnclosingClass$1 extends Thread {
          String shared;
          public EnclosingClass$1(String shared) {
              this.shared = shared;
          }
      
          public void run() {
              System.out.println(shared);
          }
      }
      

      如您所见,独立类持有对共享对象的引用,记住 java 中的所有内容都是按值传递的,因此即使 EnclosureClass 中的引用变量 'shared' 发生变化,它指向的实例没有被修改,所有其他指向它的引用变量(比如匿名类中的一个:Enclosure$1),都不会意识到这一点。这是编译器强制你将这个“共享”变量声明为 final 的主要原因,这样这种行为就不会出现在你已经运行的代码中。

      现在,当您在匿名类中使用实例变量时会发生这种情况(这是您应该做的解决问题的方法,将您的逻辑移至“实例”方法或类的构造函数):

      public class EnclosingClass {
          String shared = "hello";
          public void someMethod() {
              new Thread() {
                  public void run() {
                      System.out.println(shared); // this is perfectly valid
                  }
              }.start();
      
              // change the reference 'shared' points to, with a new value
              shared = "other hello"; 
              System.out.println(shared);
          }
      }
      

      这编译得很好,因为编译器会修改代码,所以新生成的类 Enclosure$1 将持有对 EnclosureClass 实例的引用(这只是一个表示,但应该让你继续):

      public void someMethod() {
          new EnclosingClass$1(this).start();
      
          // change the reference 'shared' points to, with a new value
          shared = "other hello"; 
          System.out.println(shared);
      }
      
      public class EnclosingClass$1 extends Thread {
          EnclosingClass enclosing;
          public EnclosingClass$1(EnclosingClass enclosing) {
              this.enclosing = enclosing;
          }
      
          public void run() {
              System.out.println(enclosing.shared);
          }
      }
      

      像这样,当 EnclosureClass 中的引用变量 'shared' 被重新分配时,并且这发生在调用 Thread#run() 之前,你会看到“other hello”打印了两次,因为现在 EnclosureClass$1#enclosing 变量将保留对声明它的类的对象的引用,因此对该对象的任何属性的更改对于 EnclosureClass$1 的实例都是可见的。

      有关该主题的更多信息,您可以查看这篇优秀的博文(不是我写的):http://kevinboone.net/java_inner.html

      【讨论】:

      • 如果局部变量“shared”是一个可变对象怎么办?根据您的解释,宣布“最终”也无济于事,对吧?
      • 将“shared”声明为 final 将允许您修改 final 变量引用的对象的状态,但是对于这个特定的示例,由于您将无法更改值,所以这将不起作用的“共享”变量(这是 OP 想要的),您将能够在匿名类中使用它,但它的值不会改变(因为它被声明为 final)。重要的是要注意变量和它们所持有的实际值之间的差异(可能是原始值或对堆中对象的引用)。
      • >>> 但它的值不会改变 我猜你错过了这一点,即如果最终引用变量指向一个可变对象,它仍然可以更新,但是,匿名类会创建浅拷贝,因此更改会反映在匿名类中。换句话说,状态是同步的,这正是这里所需要的。在这里,OP 需要修改共享变量(原始类型)的能力,并实现该 OP 需要将值包装在可变对象下并共享该可变对象。
      • 当然,OP 可以将所需的值包装在可变对象下,将变量声明为 final 并使用它。但是,他可以通过将变量声明为当前类的属性来避免使用额外的对象(如答案中所指出和解释的那样)。强制可变对象(例如使用数组来修改共享变量的值)不是一个好主意。
      【解决方案6】:

      当我偶然发现这个问题时,我只是通过构造函数将对象传递给内部类。如果我需要传递原语或不可变对象(如本例所示),则需要一个包装类。

      编辑:实际上,我根本不使用匿名类,而是使用适当的子类:

      public class PriceData {
              private double lastPrice = 0;
              private double price = 0;
      
              public void setlastPrice(double lastPrice) {
                  this.lastPrice = lastPrice;
              }
      
              public double getLastPrice() {
                  return lastPrice;
              }
      
              public void setPrice(double price) {
                  this.price = price;
              }
      
              public double getPrice() {
                  return price;
              }
          }
      
          public class PriceTimerTask extends TimerTask {
              private PriceData priceData;
              private Price priceObject;
      
              public PriceTimerTask(PriceData priceData, Price priceObject) {
                  this.priceData = priceData;
                  this.priceObject = priceObject;
              }
      
              public void run() {
                  priceData.setPrice(priceObject.getNextPrice(lastPrice));
                  System.out.println();
                  priceData.setLastPrice(priceData.getPrice());
      
              }
          }
      
          public static void main(String args[]) {
      
              int period = 2000;
              int delay = 2000;
      
              PriceData priceData = new PriceData();
              Price priceObject = new Price();
      
              Timer timer = new Timer();
      
              timer.scheduleAtFixedRate(new PriceTimerTask(priceData, priceObject), delay, period);
          }
      

      【讨论】:

        【解决方案7】:

        您不能引用非最终变量,因为 Java 语言规范是这样说的。从 8.1.3 开始:
        “任何使用但未在内部类中声明的局部变量、形式方法参数或异常处理程序参数都必须声明为 final。” Whole paragraph.
        我只能看到您的部分代码-根据我的说法,调度局部变量的修改是一个奇怪的想法。当您离开函数时,局部变量将不复存在。也许类的静态字段会更好?

        【讨论】:

          【解决方案8】:

          我只是写了一些东西来处理符合作者意图的东西。 我发现最好的办法是让构造函数获取所有对象,然后在您实现的方法中使用该构造函数对象。

          但是,如果您正在编写一个通用接口类,那么您必须传递一个对象,或者最好是一个对象列表。这可以通过 Object[] 甚至更好的方法来完成,Object ... 因为它更容易调用。

          请看下面我的示例。

          List<String> lst = new ArrayList<String>();
          lst.add("1");
          lst.add("2");        
          
          SomeAbstractClass p = new SomeAbstractClass (lst, "another parameter", 20, true) {            
          
              public void perform( ) {                           
                  ArrayList<String> lst = (ArrayList<String>)getArgs()[0];                        
              }
          
          };
          
          public abstract class SomeAbstractClass{    
              private Object[] args;
          
              public SomeAbstractClass(Object ... args) {
                  this.args = args;           
              }      
          
              public abstract void perform();        
          
              public Object[] getArgs() {
                  return args;
              }
          
          }
          

          请参阅这篇关于开箱即用的 Java 闭包的文章: http://mseifed.blogspot.se/2012/09/closure-implementation-for-java-5-6-and.html

          版本 1 支持通过自动转换传递非最终闭包:
          https://github.com/MSeifeddo/Closure-implementation-for-Java-5-6-and-7/blob/master/org/mo/closure/v1/Closure.java

              SortedSet<String> sortedNames = new TreeSet<String>();
              // NOTE! Instead of enforcing final, we pass it through the constructor
              eachLine(randomFile0, new V1<String>(sortedNames) {
                  public void call(String line) {
                      SortedSet<String> sortedNames = castFirst();  // Read contructor arg zero, and auto cast it
                      sortedNames.add(extractName(line));
                  }
              });
          

          【讨论】:

            【解决方案9】:

            如果你想在匿名类的方法调用中改变一个值,那个“值”实际上是一个Future。所以,如果你使用 Guava,你可以写

            ...
            final SettableFuture<Integer> myvalue = SettableFuture<Integer>.create();
            ...
            someclass.run(new Runnable(){
            
                public void run(){
                    ...
                    myvalue.set(value);
                    ...
                }
             }
            
             return myvalue.get();
            

            【讨论】:

              【解决方案10】:

              没有提到我注意到的一个解决方案(除非我错过了它,如果我确实请纠正我),是使用类变量。尝试在方法中运行新线程时遇到此问题:new Thread(){ Do Something }

              从以下调用doSomething() 将起作用。你不一定要声明它final,只需要改变变量的范围,这样它就不会在内部类之前被收集。当然,除非您的流程非常庞大,并且更改范围可能会产生某种冲突。我不想将我的变量设为 final,因为它绝不是 final/constant。

              public class Test
              {
              
                  protected String var1;
                  protected String var2;
              
                  public void doSomething()
                  {
                      new Thread()
                      {
                          public void run()
                          {
                              System.out.println("In Thread variable 1: " + var1);
                              System.out.println("In Thread variable 2: " + var2);
                          }
                      }.start();
                  }
              
              }
              

              【讨论】:

                【解决方案11】:

                如果变量需要是最终的,那么您可以将变量的值分配给另一个变量并使其成为最终的,以便您可以使用它。

                【讨论】:

                  【解决方案12】:

                  使用 ClassName.this.variableName 引用非最终变量

                  【讨论】:

                    【解决方案13】:

                    您可以在外部类之外声明变量。在此之后,您将能够从内部类中编辑变量。我在 android 中编码时有时会遇到类似的问题,所以我将变量声明为全局变量,它对我有用。

                    【讨论】:

                    • 这并不能真正回答这个问题......这就是你被否决的原因。
                    【解决方案14】:

                    你能制作匿名内部类的lastPricepriceObjectprice字段吗?

                    【讨论】:

                      【解决方案15】:

                      主要关注的是匿名类实例中的变量是否可以在运行时解析。只要保证变量在运行时范围内,就不必将变量设为最终变量。例如,请看 updateStatus() 方法中的两个变量 _statusMessage 和 _statusTextView。

                      public class WorkerService extends Service {
                      
                      Worker _worker;
                      ExecutorService _executorService;
                      ScheduledExecutorService _scheduledStopService;
                      
                      TextView _statusTextView;
                      
                      
                      @Override
                      public void onCreate() {
                          _worker = new Worker(this);
                          _worker.monitorGpsInBackground();
                      
                          // To get a thread pool service containing merely one thread
                          _executorService = Executors.newSingleThreadExecutor();
                      
                          // schedule something to run in the future
                          _scheduledStopService = Executors.newSingleThreadScheduledExecutor();
                      }
                      
                      @Override
                      public int onStartCommand(Intent intent, int flags, int startId) {
                      
                          ServiceRunnable runnable = new ServiceRunnable(this, startId);
                          _executorService.execute(runnable);
                      
                          // the return value tells what the OS should
                          // do if this service is killed for resource reasons
                          // 1. START_STICKY: the OS restarts the service when resources become
                          // available by passing a null intent to onStartCommand
                          // 2. START_REDELIVER_INTENT: the OS restarts the service when resources
                          // become available by passing the last intent that was passed to the
                          // service before it was killed to onStartCommand
                          // 3. START_NOT_STICKY: just wait for next call to startService, no
                          // auto-restart
                          return Service.START_NOT_STICKY;
                      }
                      
                      @Override
                      public void onDestroy() {
                          _worker.stopGpsMonitoring();
                      }
                      
                      @Override
                      public IBinder onBind(Intent intent) {
                          return null;
                      }
                      
                      class ServiceRunnable implements Runnable {
                      
                          WorkerService _theService;
                          int _startId;
                          String _statusMessage;
                      
                          public ServiceRunnable(WorkerService theService, int startId) {
                              _theService = theService;
                              _startId = startId;
                          }
                      
                          @Override
                          public void run() {
                      
                              _statusTextView = MyActivity.getActivityStatusView();
                      
                              // get most recently available location as a latitude /
                              // longtitude
                              Location location = _worker.getLocation();
                              updateStatus("Starting");
                      
                              // convert lat/lng to a human-readable address
                              String address = _worker.reverseGeocode(location);
                              updateStatus("Reverse geocoding");
                      
                              // Write the location and address out to a file
                              _worker.save(location, address, "ResponsiveUx.out");
                              updateStatus("Done");
                      
                              DelayedStopRequest stopRequest = new DelayedStopRequest(_theService, _startId);
                      
                              // schedule a stopRequest after 10 seconds
                              _theService._scheduledStopService.schedule(stopRequest, 10, TimeUnit.SECONDS);
                          }
                      
                          void updateStatus(String message) {
                              _statusMessage = message;
                      
                              if (_statusTextView != null) {
                                  _statusTextView.post(new Runnable() {
                      
                                      @Override
                                      public void run() {
                                          _statusTextView.setText(_statusMessage);
                      
                                      }
                      
                                  });
                              }
                          }
                      
                      }
                      

                      【讨论】:

                        【解决方案16】:

                        对我有用的只是在你的这个函数之外定义变量。

                        就在主函数声明之前,即

                        Double price;
                        public static void main(String []args(){
                        --------
                        --------
                        }
                        

                        【讨论】:

                        • 那不行,你要声明一个实例变量,要使用它,你需要在你的 main 方法中创建一个实例。您应该更具体,或者简单地将静态修饰符添加到“价格”变量。
                        【解决方案17】:

                        将变量声明为静态变量并使用 className.variable 在所需方法中引用它

                        【讨论】:

                        • Non-static parameter cannot be referenced from a static context
                        • @Shweta 局部变量和方法参数不能被声明为“静态”,此外,它的实现方式是允许方法中的类(本地匿名类)继续访问本地变量和方法参数,即使在方法返回之后,它也会制作它们的“最终”副本并将它们用作实例变量。
                        【解决方案18】:

                        只是另一种解释。考虑下面这个例子

                        public class Outer{
                             public static void main(String[] args){
                                 Outer o = new Outer();
                                 o.m1();        
                                 o=null;
                             }
                             public void m1(){
                                 //int x = 10;
                                 class Inner{
                                     Thread t = new Thread(new Runnable(){
                                         public void run(){
                                             for(int i=0;i<10;i++){
                                                 try{
                                                     Thread.sleep(2000);                            
                                                 }catch(InterruptedException e){
                                                     //handle InterruptedException e
                                                 }
                                                 System.out.println("Thread t running");                             
                                             }
                                         }
                                     });
                                 }
                                 new Inner().t.start();
                                 System.out.println("m1 Completes");
                            }
                        }
                        

                        这里输出将是

                        m1 完成

                        线程t运行

                        线程t运行

                        线程t运行

                        .......

                        现在方法 m1() 完成,我们将引用变量 o 分配给 null ,现在外部类对象有资格进行 GC,但内部类对象仍然存在,它与正在运行的线程对象具有 (Has-A) 关系。没有现有的外部类对象,就没有现有的 m1() 方法,没有现有的 m1() 方法,就没有机会存在其局部变量,但是如果内部类对象使用 m1() 方法的局部变量,那么一切都是不言自明的.

                        为了解决这个问题,我们必须创建局部变量的副本,然后必须将其复制到带有内部类对象的堆中,java 仅对最终变量所做的事情,因为它们实际上不是变量,它们就像常量(一切都发生在仅编译时而不是在运行时)。

                        【讨论】:

                          【解决方案19】:

                          为了解决上述问题,不同的语言会做出不同的决定。

                          对于 Java,解决方案与我们在本文中看到的一样。

                          对于 C#,解决方案是允许副作用,通过引用捕获是唯一的选择。

                          对于 C++11,解决方案是让程序员做出决定。他们可以选择按价值或按参考进行捕获。如果按值捕获,则不会出现副作用,因为引用的变量实际上是不同的。如果通过引用捕获,可能会出现副作用,但程序员应该意识到这一点。

                          【讨论】:

                            【解决方案20】:

                            因为如果变量不是最终变量会令人困惑,因为对它的更改不会在匿名类中被拾取。

                            只需将变量 'price' 和 'lastPrice' 设为 final。

                            -- 编辑

                            哎呀,显然,在您的函数中,您也不需要分配给它们。您将需要新的局部变量。无论如何,我怀疑现在有人已经给你一个更好的答案了。

                            【讨论】:

                            • 这不仅令人困惑 - 它完全不正确,因此编译器不允许这样做。
                            • 但是当我需要时如何更改值?
                            • 不仅仅是因为它令人困惑;这是因为 Java 不支持闭包。请看下面我的回答。 @Ankur:您可以将变量设为匿名类对象的成员变量,而不是 main() 中的局部变量。
                            • 他正在修改它们,所以它们不能是最终的。
                            • 如果 price 和 lastPrice 是最终的,分配给它们的分配将不会编译。
                            猜你喜欢
                            • 2014-12-09
                            • 1970-01-01
                            • 2012-12-19
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            相关资源
                            最近更新 更多