无法在用其他方法定义的内部类中引用非最终变量


247

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

我以前写过下面的内容:

我收到错误消息“无法引用用其他方法定义的内部类内部的非最终变量”。

对于名为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

1
@Ankur:简单的答案是“否”。但是,您可以使用内部类来达到所需的效果。参见@petercardona的答案。
Stephen C

Answers:


197

Java不支持真正闭包,即使使用匿名类像你在这里使用(new TimerTask() { ... }就像是一种封闭的)的外观。

编辑 - 请参阅下面的注释-正如KeeperOfTheSoul所指出的,以下内容不是正确的解释。

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

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

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

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

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

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

@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
并非完全正确,Java确实会为有问题的变量生成捕获以捕获其运行时值,只是它们希望避免在.Net中可能出现的奇怪副作用,即在捕获代理中的值后更改Java可以避免的这种行为的C#示例,现在委托可以看到新值,请参见stackoverflow.com/questions/271440/c-captured-variable-in-loop
克里斯·奇尔弗斯

14
这不是“奇怪的副作用”,而是人们期望的正常行为-Java无法交付,因为它不生成捕获。解决方法是,匿名类中使用的局部变量必须是最终变量。
Michael Borgwardt

12
Jesper,您可能应该删除答案中不正确的部分,而不是仅收到一条消息,指出以上内容不正确。
詹姆斯·麦克马洪

19
实际上,Java不支持闭包。支持闭包的语言通过将整个本地环境(即,在当前堆栈框架中定义的一组本地变量)存储为堆对象来实现此目的。Java没有对此的支持(语言设计人员希望实现它,但是用完了),因此,作为一种解决方法,每当实例化一个本地类时,它所引用的任何本地变量的值都会复制到堆中。但是,JVM无法使这些值与局部变量保持同步,这就是为什么它们必须是最终变量的原因。
Taymon'3

64
现在,没有人对“ KeeperOfTheSoul”发表评论,这个答案完全令人困惑。答案应该修改。
亚当·帕金

32

为了避免匿名委托引用的java变量中的闭包带来奇怪的副作用,必须将其标记为final,因此请参阅 lastPrice在timer任务中和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并从计时器中调用.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
或者您可以带领Foo实施Runnable ..?
vidstige 2014年

18

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

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

要么:

有一个针对您的lastPriceprice变量的快速(并且在我看来是丑陋的)黑客,它像这样声明

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

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

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

14

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

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

似乎您可能会做得更好,但是想法是可以将更新的变量分组在不变的类引用中。


11

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

以下面的代码为例:

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

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


如果局部变量“ shared”是可变对象怎么办?根据您的解释,声明“最终”也无济于事,对吧?
sactiw '17

将“ shared”声明为final将允许您修改final变量引用的对象的状态,但是对于此特定示例而言,该操作无效,因为您将无法更改“ shared”变量的值(是OP想要的),您将可以在匿名类中使用它,但是它的值不会改变(因为它被声明为final)。重要的是要注意变量和变量所持有的实际值之间的差异(它们可能是基元或对堆中对象的引用)。
emerino '17

>>>但它的值不会改变, 我想您可能遗漏了要点,即如果最终引用变量指向可变对象,它仍然可以更新,但是,匿名类创建了浅表副本,因此更改反映在匿名中类。换句话说,状态是同步的,这是这里所需要的。在这里,OP需要具有修改共享变量(原始类型)并实现OP将值包装在可变对象下并共享该可变对象的能力。
sactiw

1
当然,OP可以将所需的值包装在可变对象下,将变量声明为final并使用该变量。但是,他可以通过将变量声明为当前类的属性来避免使用额外的对象(如答案中所指出和解释的)。强制使用可变对象(例如仅使用数组即可修改共享变量的值)不是一个好主意。
emerino '17

7

当我偶然发现此问题时,我只是通过构造函数将对象传递给内部类。如果需要传递原语或不可变的对象(在这种情况下),则需要包装器类。

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

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

2

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


2

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

但是,如果要编写通用接口类,则必须传递一个对象,或者最好传递一个对象列表。这可以通过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

如果要在匿名类内的方法调用中更改值,则该“值”实际上是Future。因此,如果您使用番石榴,则可以编写

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

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

 return myvalue.get();

2

我没有注意到的一种解决方案是使用类变量(除非我错过了,否则请改正)。尝试在方法中运行新线程时遇到此问题: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();
    }

}

1

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



1

您可以在外部类外部声明变量。之后,您将可以在内部类中编辑变量。在android中进行编码时,有时会遇到类似的问题,因此我将变量声明为global,并且对我有用。


这并不能真正回答问题……这就是为什么您会被拒绝。
Stuart Siegler


0

主要问题是匿名类实例内部的变量是否可以在运行时解析。只要确保变量在运行时范围内,就不必将变量定为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

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

就在主函数声明即

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

那是行不通的,您要声明一个实例变量,要使用它,您需要在main方法内创建一个实例。您应该更具体,或者将静态修饰符添加到“ price”变量中。
emerino 2014年

0

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


Non-static parameter cannot be referenced from a static context
斯蒂芬,2015年

@Shweta局部变量和方法参数不能被声明为“静态”,而且,它是关于实现方法的一种方法,该方法允许方法中的类(局部匿名类)即使在方法之后也可以继续访问局部变量和方法参数已经返回,即它会复制其“最终”副本并将其用作实例变量。
sactiw

0

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

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

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


-1

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

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

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

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


-2

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

只需将变量“ price”和“ lastPrice”定为final即可。

-编辑

糟糕,您显然也不必在您的函数中分配给他们。您将需要新的局部变量。无论如何,我怀疑现在有人给了您更好的答案。


2
它不仅令人困惑-其完全错误,因此编译器不允许这样做。
CHII

但是,当需要时如何更改值?
Ankur

不只是因为它令人困惑;这是因为Java不支持闭包。请参阅下面的答案。@Ankur:您可以使变量成为匿名类对象的成员变量,而不是main()中的局部变量。
杰斯珀,

他正在修改它们,因此它们不可能是最终的。
罗宾

如果price和lastPrice是最终的,则不会编译对它们的赋值。
格雷格·马特斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.