在不同方法中定义的内部类无法引用非最终变量。

251

编辑:

我需要在计时器运行多次的过程中更改多个变量的值。我需要在每次通过计时器迭代时更新这些值。我不能将这些值设置为final,因为那样会防止我更新它们,然而我正在遇到下面描述的错误:

我之前写的是:

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

这发生在名为price和名为priceObject的double变量上。你知道我为什么会遇到这个问题吗?我不明白为什么需要有一个final声明。如果你能看出我试图做什么,那么我需要怎么做才能解决这个问题。

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
@StephenC 抱歉,但是现在这个petercardona是谁? - wlnirvana
我很惊讶你需要问那个问题... - Stephen C
20个回答

199

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

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

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

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

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

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

通过将lastPriceprice设置为final,它们实际上不再是变量,而是常量。编译器可以使用常量的值(当然是在编译时)替换匿名类中对lastPriceprice的使用,这样就不会再出现访问不存在变量的问题了。

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

@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中可能出现的奇怪副作用,即您捕获委托中的值,在外部方法中更改该值,现在委托看到了新值,请参见https://dev59.com/DXVC5IYBdhLWcg3whBaj以获取C#示例,说明Java试图避免这种行为。 - Chris Chilvers
14
这并不是一个“奇怪的副作用”,而是人们期望的正常行为 - 而Java无法实现这一点,因为它不生成捕获。作为解决方法,匿名类中使用的本地变量必须是final的。 - Michael Borgwardt
12
Jesper,你应该将回答中的错误部分进行编辑,而不是只留下一条消息说上面的内容是错误的。请注意保持原意并使翻译通俗易懂。 - James McMahon
19
实际上,Java并不支持闭包。那些支持闭包的语言通过将整个本地环境(即当前堆栈帧中定义的本地变量集合)存储为堆对象来实现。Java没有对此提供支持(语言设计者本打算实现它,但时间不够),因此,每当实例化本地类时,它所引用的任何本地变量的值都会被复制到堆上,作为一种解决方法。然而,JVM无法将这些值与本地变量保持同步,这就是为什么它们必须是final的原因。 - Taymon
67
由于没有名为“KeeperOfTheSoul”的人对此进行了评论,因此这个答案现在完全令人困惑。应该对答案进行修订。 - Adam Parkin
显示剩余24条评论

32
为了避免Java闭包中的奇怪副作用,被匿名委托引用的变量必须标记为final,所以要在计时器任务中引用lastPrice和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;
    }
}

现在只需创建一个 final 的新 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

19
您只能在使用匿名类时从包含类中访问final变量。因此,您需要声明正在使用的变量为final(由于更改了lastPrice和price,这不是您的选项),或者不使用匿名类。

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

或者:

对于您的lastPrice和price变量,有一个快速(而且我认为很丑)的hack方法,即将其声明为以下形式

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)也不会意识到这一点。这就是编译器强制您将这些“shared”变量声明为final的主要原因,以便这种类型的行为不会进入已运行的代码。
现在,当您在匿名类中使用实例变量时(这是解决问题的方法,将逻辑移动到“instance”方法或类的构造函数中),会发生以下情况:
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”是可变对象怎么办?根据您的解释,声明“final”也无济于事,对吗? - sactiw
将“shared”声明为final将允许您修改final变量引用的对象的状态,但对于这个特定的示例,这不起作用,因为您无法更改“shared”变量的值(这是OP所需的),您将能够在匿名类中使用它,但其值不会改变(因为它被声明为final)。重要的是要注意变量与它们所持有的实际值(可以是基元或堆中对象的引用)之间的区别。 - emerino
但它的值不会改变 我猜你没有理解重点,即如果最终引用变量指向可变对象,则仍然可以更新它,但是匿名类创建浅拷贝,因此更改会反映在匿名类中。换句话说,状态是同步的,这是所需的。在这里,OP需要能够修改共享变量(原始类型),为了实现这一点,OP将需要将该值包装在可变对象下并共享该可变对象。
- sactiw
1
当然,OP可以将所需的值封装在可变对象中,将变量声明为final,并使用该变量。但是,他可以通过将变量声明为当前类的属性来避免使用额外的对象(正如答案中指出并解释的那样)。强制使用可变对象(例如仅为了能够修改共享变量的值而使用数组)不是一个好主意。 - emerino

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
我刚刚写了一些东西来处理作者意图中的某些内容。我发现最好的方法是让构造函数接收所有对象,然后在你实现的方法中使用该构造函数中的对象。
然而,如果你正在编写通用接口类,那么你必须传递一个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支持传递非final闭包并进行自动转换:
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

我注意到一个未被提及的解决方案(如果我有所遗漏,请纠正我),即使用类变量。在方法中运行新线程时遇到了这个问题:new Thread(){ Do Something }

从以下代码调用doSomething()将会起作用。您不一定需要将其声明为final,只需要改变变量的范围,以便在内部类之前不被收集。当然,如果您的进程非常庞大,改变范围可能会产生某种冲突。我的变量并不是最终/恒定的,因此我不想把它设置为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

如果您想在匿名类中更改方法调用中的值,那么该“值”实际上是一个Future。因此,如果您使用Guava,则可以编写以下代码:

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

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

 return myvalue.get();

2

您可以在外部类之外声明变量。这样,您就可以从内部类中编辑变量。我有时在编写Android代码时遇到类似的问题,所以我将变量声明为全局变量,这对我很有效。


这并没有真正回答问题... 这就是你被踩的原因。 - Stuart Siegler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接