不能引用以其他方法定义的内部类中的非最终变量

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

提问人:Ankur 提问时间:8/19/2009 最后编辑:CommunityAnkur 更新时间:9/7/2017 访问量:206427

问:

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

我之前写过以下内容:

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

这发生在称为 price 的双倍和称为 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);
}
Java 方法 声明 final

评论

0赞 Ankur 8/19/2009
我要问的是,如何在计时器中获取可以不断更新的变量。
1赞 Stephen C 8/19/2009
@Ankur:简单的答案是“不”。但是您可以使用内部类来实现所需的效果;请参阅@petercardona的回答。
0赞 wlnirvana 6/10/2022
@StephenC对不起,但现在这个彼得卡多纳是谁?
0赞 Stephen C 6/10/2022
stackoverflow.com/a/1299947/139985。我很惊讶你需要问这个问题......

答:

-2赞 Noon Silk 8/19/2009 #1

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

只需将变量 'price' 和 'lastPrice' 设置为最终值即可。

--编辑

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

评论

2赞 Chii 8/19/2009
它不仅令人困惑 - 它完全不正确,因此编译器不允许它。
0赞 Ankur 8/19/2009
但是,在需要时如何更改值呢?
0赞 Jesper 8/19/2009
不仅仅是因为它令人困惑;这是因为 Java 不支持闭包。请参阅下面的回答。@Ankur:您可以使变量成为匿名类对象的成员变量,而不是 main() 中的局部变量。
0赞 Robin 8/19/2009
他正在修改它们,所以它们不可能是最终的。
0赞 Greg Mattes 8/19/2009
如果 price 和 lastPrice 是最终的,则不会编译对它们的赋值。
199赞 Jesper 8/19/2009 #2

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

编辑 - 请参阅下面的评论 - 正如 KeeperOfTheSoul 指出的那样,以下不是正确的解释。

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

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

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

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

通过 和 ,它们不再是真正的变量,而是常量。然后,编译器可以将匿名类中的 and 替换为常量的值(当然,在编译时),您将不再遇到访问不存在的变量的问题。lastPricepricefinallastPriceprice

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

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

评论

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

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

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

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

评论

1赞 vidstige 12/13/2014
或者你可以直接领导 Foo 实现 Runnable..?
19赞 Robin 8/19/2009 #4

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

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

或:

对于您的lastPrice价格变量,有一个快速(在我看来是丑陋的)hack,可以这样声明它

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

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

price[0] = priceObject.getNextPrice(lastPrice[0]);
System.out.println();
lastPrice[0] = price[0];
0赞 Greg Mattes 8/19/2009 #5

你能使匿名内部类的 、 和 字段吗?lastPricepriceObjectprice

2赞 Tadeusz Kopec for Ukraine 8/19/2009 #6

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

14赞 Peter Cardona 8/19/2009 #7

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

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

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

7赞 Buhb 8/19/2009 #8

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

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

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赞 Thorbjørn Ravn Andersen 8/19/2009 #9

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

2赞 mjs 10/5/2011 #10

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

但是,如果你正在编写一个泛型接口类,那么你必须传递一个 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));
        }
    });
-1赞 Earth Engine 10/19/2011 #11

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

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

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

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

2赞 Earth Engine 5/16/2013 #12

如果要更改匿名类中方法调用中的值,则该“值”实际上是 .所以,如果你使用番石榴,你可以写Future

...
final SettableFuture<Integer> myvalue = SettableFuture<Integer>.create();
...
someclass.run(new Runnable(){

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

 return myvalue.get();
0赞 WaiKit Kung 5/23/2013 #13

主要关注的是匿名类实例中的变量是否可以在运行时解析。只要保证变量在运行时范围内,就不必将变量设为 final。例如,请参阅 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);

                }

            });
        }
    }

}
0赞 punkck 5/26/2013 #14

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

就在 main 函数声明之前,即

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

评论

0赞 emerino 12/24/2014
这是行不通的,你正在声明一个实例变量,要使用它,你需要在你的 main 方法中创建一个实例。您应该更具体,或者只是将静态修饰符添加到“price”变量中。
1赞 user3251651 8/18/2014 #15

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

0赞 shweta 9/23/2014 #16

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

评论

0赞 tread 3/16/2015
Non-static parameter cannot be referenced from a static context
0赞 sactiw 6/18/2015
@Shweta局部变量和方法参数不能被声明为“静态”,而且,它与它的实现方式有关,允许方法中的类(局部匿名类)继续访问局部变量和方法参数,即使在方法返回后也是如此,即它制作它们的“最终”副本并将它们用作实例变量。
11赞 emerino 9/29/2014 #17

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

以以下代码为例:

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), 不会意识到这一点。这是编译器强制您将此“共享”变量声明为最终变量的主要原因,这样这种类型的行为就不会进入您已经运行的代码中。

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

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() 之前,你会看到 “other hello” 打印了两次,因为现在 EnclosingClass$1#enclosing 变量将保留对声明它的类对象的引用,因此对该对象上任何属性的更改将对 EnclosingClass$1 的实例可见。

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

评论

0赞 sactiw 4/19/2017
如果局部变量“shared”是可变对象怎么办?根据你的解释,宣布“最终”也无济于事,对吧?
0赞 emerino 4/20/2017
将“shared”声明为 final 将允许您修改 final 变量引用的对象的状态,但对于这个不起作用的特定示例,因为您将无法更改“shared”变量的值(这是 OP 想要的),您将能够在匿名类中使用它,但它的值不会更改(因为它被声明为 final)。重要的是要注意变量和它们所包含的实际值(可能是基元或对堆中对象的引用)之间的差异。
0赞 sactiw 4/20/2017
>>>,但它的价值不会改变我想你错过了这一点,即如果最终的引用变量指向一个可变对象,它仍然可以更新,但是,匿名类会创建浅拷贝,因此更改会反映在匿名类中。换句话说,状态是同步的,这是这里所需要的。在这里,OP 需要能够修改共享变量(基元类型),并且为了实现这一点,OP 需要将值包装在一个可变对象下并共享该可变对象。
1赞 emerino 4/21/2017
当然,OP 可以将所需的值包装在一个可变对象下,将变量声明为 final,然后使用它。但是,他可以通过将变量声明为当前类的属性来避免使用额外的对象(如答案中指出和解释的那样)。强制可变对象(例如使用数组只是为了能够修改共享变量的值)不是一个好主意。
2赞 James 12/24/2014 #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();
    }

}
2赞 Abhijay Ghildyal 9/2/2015 #19

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

评论

0赞 Stuart Siegler 9/2/2015
这并不能真正回答这个问题......这就是你被否决的原因。
0赞 pks 7/5/2016 #20

只是另一种解释。请看下面的例子

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 条件,但内部类对象仍然存在,它与正在运行的 Thread 对象有 (Has-A) 关系。没有现有的外部类对象,就没有现有的 m1() 方法,没有现有的 m1() 方法,就没有机会存在其局部变量,但如果内部类对象使用 m1() 方法的局部变量,那么一切都是不言自明的。

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