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

Cannot refer to a non-final variable inside an inner class defined in a different method

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

我以前写过以下内容:

I am getting the error"cannot refer to a non-final variable inside an inner class defined in a different method".

This is happening for the double called price and the Price called priceObject. Do you know why I get this problem. I do not understand why I need to have a final declaration. Also if you can see what it is I am trying to do, what do I have to do to get around this problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}


Java不支持真正的闭包,即使使用像你在这里使用的匿名类(EDCOX1,0),看起来还是一种闭包。

编辑-见下面的评论-下面的解释不正确,正如Keeperof指出的。

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

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

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

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

通过使lastPricepricefinal,它们不再是真正的变量,而是常量。然后,编译器就可以用常量的值(当然,在编译时)替换匿名类中使用lastPriceprice,这样就不再有访问不存在变量的问题了。

其他支持闭包的编程语言通过专门处理这些变量来实现这一点——确保在方法结束时不会破坏这些变量,这样闭包仍然可以访问这些变量。

@你可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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变量中出现的异常副作用,必须标记为最终的,因此要引用EDCOX1×1和在它们需要标记为最终的定时器任务内的价格。

这显然对您不起作用,因为您希望更改它们,在本例中,您应该考虑将它们封装在类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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并调用。从计时器中选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}


使用匿名类时,只能从包含类访问最终变量。因此,您需要声明使用的变量是final(这对您来说不是一个选项,因为您正在更改lastprice和price),或者不要使用匿名类。

因此,您的选项是创建一个实际的内部类,在该类中,您可以传入变量并以正常方式使用它们。

或:

对于你方最后的价格和价格变量,有一个快速的(我认为是丑陋的)攻击,它是这样宣布的。

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

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

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


关于为什么你不能做你想做的事情的好的解释已经提供了。作为解决方案,可以考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 );
    }
}

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


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

以该代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
    }
}

这不起作用,因为编译器就是这样做的:

1
2
3
4
5
6
7
8
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);
}

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

1
2
3
4
5
6
7
8
9
10
public class EnclosingClass$1 extends Thread {
    String shared;
    public EnclosingClass$1(String shared) {
        this.shared = shared;
    }

    public void run() {
        System.out.println(shared);
    }
}

如您所见,独立类持有对共享对象的引用,请记住Java中的所有内容都是按值传递的,因此即使在封闭类中的引用变量"共享"被更改,它指向的实例也不会被修改,并且指向它的所有其他引用变量(如匿名类中的一个:包围1美元),Wi我不知道。这是编译器强制您将这个"共享"变量声明为最终变量的主要原因,这样这种类型的行为就不会进入已经在运行的代码中。

现在,这就是在匿名类中使用实例变量时发生的情况(这是解决问题、将逻辑移动到"实例"方法或类的构造函数时应该做的事情):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
    }
}

这编译得很好,因为编译器将修改代码,以便包含$1的新生成的类将包含对它被实例化的enclosingClass实例的引用(这只是一种表示,但应该让您继续):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
    }
}

像这样,当enclosing class中的引用变量'shared'被重新分配,并且这种情况发生在对thread run()的调用之前,您将看到打印了两次"other hello",因为现在enclosing class$1 enclosing变量将保留对声明它的类的对象的引用,因此对该对象上任何属性的更改都将可见。le到enclosingclass$1的实例。

关于这个主题的更多信息,你可以看到这个优秀的博客帖子(不是我写的):http://kevinboone.net/java_inner.html


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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);
    }

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

但是,如果您正在编写一个通用接口类,那么您必须传递一个对象,或者更好地传递一个对象列表。这可以由对象[]或更好的对象来完成,对象…因为打电话比较容易。

请看下面我的示例文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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-和.html

版本1支持通过自动铸造的非最终封口:https://github.com/mseifeddo/closing-implementation-for-java-5-6-and-7/blob/master/org/mo/closing/v1/closing.java

1
2
3
4
5
6
7
8
    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));
        }
    });

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

从以下位置调用EDOCX1[1]将有效。您不必声明它final,只需要更改变量的范围,这样就不会在内循环之前收集它。当然,除非您的过程非常庞大,并且更改范围可能会造成某种冲突。我不想让变量成为最终变量,因为它绝不是最终/常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
    }

}

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


如果要更改匿名类中方法调用的值,则该"值"实际上是一个Future。所以,如果你用番石榴,你可以写

1
2
3
4
5
6
7
8
9
10
11
12
13
...
final SettableFuture<Integer> myvalue = SettableFuture<Integer>.create();
...
someclass.run(new Runnable(){

    public void run(){
        ...
        myvalue.set(value);
        ...
    }
 }

 return myvalue.get();

如果变量必须是final,则不能是final,则可以将变量的值赋给另一个变量,并使其成为final,以便可以使用它。


使用classname.this.variablename引用非最终变量


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


></P><P>只是另一种解释。考虑下面这个例子</P></p>
<div class=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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赋给空,现在外部类对象可以用于gc,但是内部类对象仍然存在,并且与正在运行的线程对象具有(has-a)关系。如果没有现有的外部类对象,就不可能存在现有的m1()方法;如果没有现有的m1()方法,就不可能存在其局部变量,但如果内部类对象使用m1()方法的局部变量,则一切都是自解释的。

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


主要关注的是匿名类实例中的变量是否可以在运行时解决。只要确保变量在运行时范围内,就不必使变量成为最终变量。例如,请参见updateStatus()方法中的两个变量"状态消息"和"状态文本视图"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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
2
3
4
5
Double price;
public static void main(String []args(){
--------
--------
}


你能把lastPricepriceObjectprice字段设置为匿名内部类吗?


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


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

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

对于C,解决方案是允许副作用,而参考捕获是唯一的选择。

对于C++ 11,解决方案是允许程序员做出决定。它们可以选择通过值或引用进行捕获。如果按值捕获,则不会产生任何副作用,因为引用的变量实际上是不同的。如果通过引用捕获,可能会发生副作用,但是程序员应该认识到它。


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

只需将变量'price'和'lastprice'设为最终值。

--编辑

很明显,在您的函数中,您还需要不分配给它们。您将需要新的局部变量。不管怎样,我怀疑现在有人给了你一个更好的答案。