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

时间:2009-08-19 13:12:38

标签: java methods declaration final

编辑: 我需要更改几个变量的值,因为它们通过计时器运行几次。我需要通过计时器每次迭代不断更新值。我无法将值设置为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 个答案:

答案 0 :(得分:195)

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

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

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

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

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

但是匿名类对象引用了这些变量。如果匿名类对象在清理完变量后尝试访问变量,那将会发生严重错误。

通过制作main()lastPrice price,它们不再是变量,而是常量。然后,编译器可以将匿名类中finallastPrice的使用替换为常量的值(当然,在编译时),并且您将无法访问非 - 已存在的变量。

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

@Ankur:你可以这样做:

price

答案 1 :(得分:30)

为避免匿名委托引用的java变量中闭包的奇怪副作用必须标记为final,因此要在计时器任务中引用lastPrice和价格,需要将它们标记为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);
}

答案 2 :(得分:18)

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

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

或:

你的 lastPrice 价格变量有一个快速(在我看来很丑陋)的黑客攻击声明它是这样的

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

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

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

答案 3 :(得分: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 );
    }
}

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

答案 4 :(得分:10)

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

以此代码为例:

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

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

答案 5 :(得分: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);
    }

答案 6 :(得分:2)

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

答案 7 :(得分:2)

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

但是,如果您正在编写通用接口类,则必须传递一个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));
        }
    });

答案 8 :(得分:2)

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

}

答案 9 :(得分:2)

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

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

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

 return myvalue.get();

答案 10 :(得分:1)

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

答案 11 :(得分:1)

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

答案 12 :(得分:1)

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

答案 13 :(得分: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,现在Outer Class Object符合GC条件,但内部类对象仍然存在,它与正在运行的Thread对象具有(Has-A)关系。没有现有的外类对象,就没有机会存在m1()方法,没有现有的m1()方法,就没有机会存在它的局部变量,但如果内部类对象使用m1()方法的局部变量那么一切都是自解释的

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

答案 14 :(得分:0)

将变量声明为静态,并使用className.variable

在所需方法中引用它

答案 15 :(得分:0)

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

就在主要功能声明之前,即

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

答案 16 :(得分: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);

                }

            });
        }
    }

}

答案 17 :(得分:0)

您可以制作匿名内部班级的lastPricepriceObjectprice字段吗?

答案 18 :(得分:-1)

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

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

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

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

答案 19 :(得分:-2)

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

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

- 编辑

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