首页 文章

不能引用在不同方法中定义的内部类中的非final变量

提问于
浏览
239

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

我以前写过以下内容:

我收到错误“无法引用在不同方法中定义的内部类中的非final变量” . 这种情况发生在双重调用价格和价格调用priceObject上 . 你知道我为什么会遇到这个问题 . 我不明白为什么我需要最后的声明 . 此外,如果你能看到我想要做的是什么,我该怎么做才能解决这个问题 .

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

20 回答

  • 2

    Java不支持true closures,即使使用像你在这里使用的匿名类( new TimerTask() { ... } )看起来像一种闭包 .

    edit - 参见下面的评论 - 以下不是正确的解释,正如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);      
    }
    
  • 0

    为了避免使用匿名委托引用的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);
    }
    
  • 7

    在使用匿名类时,您只能从包含类访问最终变量 . 因此,您需要声明最终使用的变量(由于您要更改lastPrice和price,因此不适合您),或者不使用匿名类 .

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

    要么:

    对你的lastPrice和price变量有一个快速(在我看来很丑陋)的黑客攻击,这就是声明它如此

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

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

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

    很好的解释为什么你不能做你想要做的事已经提供 . 作为解决方案,可以考虑:

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

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

  • 0

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

    以此代码为例:

    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中的所有内容都是按值传递的,因此即使EnclosingClass中的引用变量“shared”发生更改,它指向的实例也不会被修改,所有其他引用变量都指向它(就像匿名类中的那个:Enclosing $ 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);
        }
    }
    

    这编译很好,因为编译器会修改代码,因此新生成的类Enclosing $ 1将保存对实例化EnclosingClass实例的引用(这只是一种表示,但应该让你去):

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

    像这样,当EnclosingClass中的引用变量'shared'被重新分配,而这发生在调用Thread#run()之前,你会看到“其他你好”打印两次,因为现在EnclosingClass $ 1#封闭变量会保留一个引用对于声明它的类的对象,因此对EnclosingClass $ 1的实例可以看到对该对象的任何属性的更改 .

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

  • 0

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

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

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

    您不能引用非最终变量,因为Java语言规范是这样说的 . 从8.1.3开始:
    "Any local variable, formal method parameter or exception handler parameter used but not declared in an inner class must be declared final." Whole paragraph.
    我只能看到你的部分代码 - 根据我的调度修改局部变量是一个奇怪的想法 . 离开函数时,局部变量不再存在 . 也许一个类的静态字段会更好?

  • -2

    我刚刚在 authors intention 写了一些东西到 handle . 我发现最好的办法是让 the constructor take 所有对象然后在你实现的方法中使用该构造函数对象 .

    但是,如果您正在编写通用接口类,则必须传递Object或更好的Object对象列表 . 这可以通过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));
            }
        });
    
  • 2

    如果要在匿名类中更改方法调用中的值,则"value"实际上是 Future . 所以,如果你使用 Guava ,你可以写

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

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

    从以下调用 doSomething() 将起作用 . 你不一定要声明它 final ,只需要改变变量的范围,这样就不会在内部类之前收集它 . 除非你的过程非常庞大,否则改变范围可能会产生某种冲突 . 我不想让我的变量最终,因为它绝不是最终/常数 .

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

    如果变量必须是最终的,那么你可以将变量的值赋值给另一个变量并使其成为最终变量,这样你就可以使用它 .

  • 0

    使用ClassName.this.variableName引用非final变量

  • 30

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

  • 1

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

  • 0

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

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

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

    就在主函数声明之前,即

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

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

  • -1

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

    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完成

    线程运行

    线程运行

    线程运行

    ................

    现在方法m1()完成,我们将引用变量o分配给null,现在外部类对象符合GC条件但内部类对象仍然存在,它与正在运行的Thread对象具有(Has-A)关系 . 没有现有的外类对象,就没有机会存在m1()方法,没有现有的m1()方法,就没有机会存在它的局部变量,但如果内部类对象使用m1()方法的局部变量那么一切都是自解释的 .

    要解决这个问题,我们必须创建一个局部变量的副本,然后必须使用Inner类对象复制到堆中,java只为最终变量做什么,因为它们实际上不是变量,它们就像常量一样(一切只在编译时发生)不是在运行时) .

  • 2

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

    对于Java,解决方案就像我们在本文中看到的那样 .

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

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

  • 10

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

    只需将变量'price'和'lastPrice'设为最终 .

    • 编辑

    哎呀,显然,你还需要在你的功能中分配它们 . 你需要新的局部变量 . 无论如何,我怀疑有人已经给你一个更好的答案 .

相关问题