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

Ank*_*kur 244 java methods final declaration

编辑:我需要更改几个变量的值,因为它们通过计时器运行几次.我需要通过计时器每次迭代不断更新值.我无法将值设置为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);
}
Run Code Online (Sandbox Code Playgroud)

Jes*_*per 196

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);      
}
Run Code Online (Sandbox Code Playgroud)

  • 这个答案完全令人困惑,因为没有人对"KeeperOfTheSoul"发表评论.答案应该修改. (62认同)
  • 不完全正确,Java确实为有问题的变量生成捕获以捕获它们的运行时值,它只是想避免在.Net中可能出现的奇怪副作用,其中通过捕获委托中的值,更改在外部方法中的值,现在委托看到新值,请参阅http://stackoverflow.com/questions/271440/c-captured-variable-in-loop,了解Java旨在避免的此行为的C#示例. (33认同)
  • 事实上,Java并不支持闭包.支持闭包的语言通过将整个本地环境(即当前堆栈帧中定义的局部变量集)存储为堆对象来实现.Java没有对此的支持(语言设计者希望实现它但是时间不够用),因此作为一种解决方法,每当实例化本地类时,它引用的任何局部变量的值都会被复制到堆上.但是,JVM无法使值与局部变量保持同步,这就是为什么它们必须是最终的. (18认同)
  • 这不是一个"奇怪的副作用",这是人们期望的正常行为 - 而Java无法提供*因为*它不会产生捕获.作为一种解决方法,匿名类中使用的局部变量必须是最终的. (14认同)
  • Jesper,您应该编辑出答案中不正确的部分,而不是仅仅发出一条消息说上述内容不正确. (12认同)
  • 没错,"常数"不是正确的表达(但IMO"捕获"也不是).Anonymous类对象只是获取值的副本. (5认同)
  • 它捕获某处的值,即使它只是在匿名类的私有隐藏字段中,因为实际值可以在运行时而不是编译时计算,这对于实际常量是不可能的. (4认同)
  • 令人惊讶的是,使用C#的人数并不令人惊讶,我想当Sun选择如何捕获变量时,他们决定通过强制执行变量来避免这种情况,这是相当恼人的并且导致需要包装器对象来获取同样的效果. (2认同)
  • @James如果我这样做,评论中的整个讨论将不再是可以理解的......在这种情况下,我将把它留在那里,并在文本顶部再写一遍. (2认同)
  • @haelix没有复制任何对象.每个最终局部变量的值存储在本地类的实例中.对于非原始变量,此值是对对象的引用,而不是对整个对象的引用. (2认同)

Chr*_*ers 31

为避免奇怪的副作用,由匿名委托引用的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在只需创建一个新的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);
}
Run Code Online (Sandbox Code Playgroud)


Rob*_*bin 18

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

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

要么:

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

final double lastPrice[1];
final double price[1];
Run Code Online (Sandbox Code Playgroud)

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

price[0] = priceObject.getNextPrice(lastPrice[0]);
System.out.println();
lastPrice[0] = price[0];
Run Code Online (Sandbox Code Playgroud)


Pet*_*ona 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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

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


eme*_*ino 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是行不通的,因为这是编译器在幕后所做的事情:

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);
}
Run Code Online (Sandbox Code Playgroud)

原始的匿名类被编译器生成的一些独立类所取代(代码不精确,但应该给你一个好主意):

public class EnclosingClass$1 extends Thread {
    String shared;
    public EnclosingClass$1(String shared) {
        this.shared = shared;
    }

    public void run() {
        System.out.println(shared);
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,独立类包含对共享对象的引用,请记住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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这编译很好,因为编译器会修改代码,因此新生成的类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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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


Buh*_*uhb 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);
    }
Run Code Online (Sandbox Code Playgroud)