別のメソッドで定義された内部クラス内の非final変数を参照できません


247

編集:タイマーによって数回実行されるため、いくつかの変数の値を変更する必要があります。タイマーを介してすべての反復で値を更新し続ける必要があります。値をfinalに設定できないため、値を更新できませんが、以下の最初の質問で説明するエラーが発生します。

私は以前に以下のものを書いていました:

「別のメソッドで定義された内部クラス内の非final変数を参照できません」というエラーが発生します。

これは、priceと呼ばれる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の答えを見てください。
スティーブンC

回答:


197

ここで使用しているような匿名クラスの使用()は一種のクロージャのように見えますが、Javaは真のクロージャをサポートしていませんnew TimerTask() { ... }

編集 - 以下のコメントを参照してください-KeeperOfTheSoulが指摘するように、以下は正しい説明ではありません。

これが機能しない理由です:

変数lastPriceと価格は、main()メソッドのローカル変数です。匿名クラスで作成したオブジェクトは、main()メソッドが戻るまで続く場合があります。

ときにmain()メソッドが復帰、(のようなローカル変数lastPriceとはprice、彼らが後にもはや存在しないように)、スタックからクリーンアップされますmain()戻ります。

ただし、匿名クラスオブジェクトはこれらの変数を参照します。クリーンアップされた後に匿名クラスオブジェクトが変数にアクセスしようとすると、事態はひどくうまくいきません。

lastPriceおよびを作成することでprice 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は問題の変数のキャプチャを生成してランタイム値をキャプチャします。デリゲートで値をキャプチャすることで。外部メソッドの値、そしてデリゲートは新しい値を参照します。Javaが回避しようとしているこの動作のC#の例については、stackoverflow.com / questions / 271440 / c -captured-variable- in -loopを参照してください。
Chris Chilvers、

14
これは「奇妙な副作用」ではなく、人々が期待する通常の動作であり、キャプチャを生成しないため Javaが提供できない動作です。回避策として、匿名クラスで使用されるローカル変数はfinalでなければなりません。
Michael Borgwardt、

12
ジェスパー、あなたはおそらくあなたが答えの間違った部分を編集するべきです。上記のことは間違っているというメッセージだけではなく。
James McMahon、

19
実際、Javaはクロージャーをサポートしていません。クロージャをサポートする言語は、ローカル環境全体(つまり、現在のスタックフレームで定義されているローカル変数のセット)をヒープオブジェクトとして格納することでこれを行います。Javaはこれをサポートしていません(言語の設計者はそれを実装したかったが時間切れになりました)。そのため、回避策として、ローカルクラスがインスタンス化されるたびに、参照するローカル変数の値がヒープにコピーされます。 。ただし、JVMは値をローカル変数と同期させることができないため、最終的な値にする必要があります。
Taymon

64
この回答は、「KeeperOfTheSoul」という名前の誰もコメントしていないため、完全に混乱しています。答えを修正する必要があります。
アダムパーキン2014年

32

匿名デリゲートによって参照される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を作成し、タイマーから.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

匿名クラスを使用している場合は、含まれているクラスからのみfinal変数にアクセスできます。したがって、最終的に使用される変数を宣言する必要があります(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の参照変数「共有」が変更されても、それが指すインスタンスは変更されません。 、およびそれを指す他のすべての参照変数(無名クラスの1つである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」が2回出力されます。これは、EnclosingClass $ 1#enclosing変数が参照を保持するためです。宣言されたクラスのオブジェクトに追加されるため、そのオブジェクトのすべての属性への変更は、EnclosingClass $ 1のインスタンスに表示されます。

この件の詳細については、この優れたブログ投稿(私が書いたものではありません)をご覧ください。http://kevinboone.net/java_inner.html


ローカル変数「共有」が可変オブジェクトである場合はどうなりますか?あなたの説明によると、「最終」を宣言することも助けにはなりませんよね?
sactiw 2017

「共有」をfinalとして宣言すると、最終的な変数が参照するオブジェクトの状態を変更できますが、この特定の例では、「共有」変数の値を変更できないため機能しません(これはOPが欲しかったものです)、あなたは匿名クラスの中でそれを使うことができますが、それは最終的に宣言されているので値は変わりません。変数とそれらが保持する実際の値(プリミティブまたはヒープ内のオブジェクトへの参照など)の違いに注意することが重要です。
emerino 2017

>>>しかし、値は変わりませ んが、ポイントが不足していると思います。つまり、最終的な参照変数が可変オブジェクトを指している場合、それは引き続き更新できますが、匿名クラスは浅いコピーを作成するため、変更は匿名に反映されますクラス。言い換えれば、状態は同期しています。これがここで望まれることです。ここで、OPには共有変数(プリミティブ型)を変更する機能が必要であり、そのOPを実現するには、値を可変オブジェクトの下にラップして可変オブジェクトを共有する必要があります。
sactiw 2017

1
もちろん、OPは必要な値を可変オブジェクトの下にラップし、変数をfinalとして宣言して、代わりにそれを使用できます。ただし、変数を現在のクラスの属性として宣言することで、余分なオブジェクトの使用を回避できます(回答で指摘および説明されています)。可変オブジェクトを強制する(配列を使用して共有変数の値を変更できるようにするなど)ことはお勧めできません。
emerino 2017

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言語仕様にそのように記述されているため、非final変数を参照することはできません。8.1.3から:
「使用されているが内部クラスで宣言されていないローカル変数、正式なメソッドパラメータ、または例外ハンドラパラメータは、最終的に宣言する必要があります。」段落全体。
私はあなたのコードの一部しか見ることができません-私によれば、ローカル変数の変更をスケジュールすることは奇妙な考えです。関数を終了すると、ローカル変数は存在しなくなります。たぶんクラスの静的フィールドが良いでしょうか?


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です。したがって、Guavaを使用する場合は、

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

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

 return myvalue.get();

2

私が気付いた1つの解決策は言及されていません(私がそれを逃した場合を除いて、私が訂正してくれれば)、クラス変数の使用です。メソッド内で新しいスレッドを実行しようとしてこの問題に遭遇しました:new Thread(){ Do Something }

doSomething()以下からの発信は可能です。必ずしも宣言する必要はありません。final変数のスコープを変更して、内部クラスの前に収集されないようにする必要があります。もちろん、これはプロセスが巨大で、スコープを変更すると何らかの競合が発生する場合を除いてです。変数がfinal / constantではないため、変数を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();
    }

}


1

ClassName.this.variableNameを使用して、最終でない変数を参照する


1

外部クラスの外で変数を宣言するだけです。この後、内部クラス内から変数を編集できるようになります。Androidでコーディングしているときに同様の問題に直面することがあるので、変数をグローバルとして宣言するとうまくいきます。


これは実際には質問に答えません。
Stuart Siegler、2015


0

主な関心事は、匿名クラスインスタンス内の変数を実行時に解決できるかどうかです。変数がランタイムスコープ内にあることが保証されている限り、変数をfinalにする必要はありません。たとえば、updateStatus()メソッド内の2つの変数_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

私のために働いたのは、あなたのこの関数の外で変数を定義することです。

メイン関数の直前にieを宣言します

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

これは機能しません。インスタンス変数を宣言しています。それを使用するには、メインメソッド内にインスタンスを作成する必要があります。より具体的にするか、単に静的な修飾子を「price」変数に追加する必要があります。
emerino 2014

0

変数を静的として宣言し、必要なメソッドでclassName.variableを使用して参照します


Non-static parameter cannot be referenced from a static context
スティーブン

@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は最終的な変数のみに対して何を行うのかは定数のようです(すべてがコンパイル時にのみ発生します)実行時ではありません)。


-1

上記の問題を解決するために、言語によって異なる決定が行われます。

Javaの場合、解決策はこの記事で説明したとおりです。

C#の場合、解決策は副作用を許可することであり、参照によるキャプチャが唯一のオプションです。

C ++ 11の場合、解決策はプログラマーが決定できるようにすることです。値または参照によってキャプチャすることを選択できます。値でキャプチャする場合、参照される変数は実際には異なるため、副作用は発生しません。参照によるキャプチャの場合、副作用が発生する可能性がありますが、プログラマはそれを理解する必要があります。


-2

変数がfinalではない場合、混乱が生じます。これは、その変更が匿名クラスで取得されないためです。

変数「price」と「lastPrice」をfinalにするだけです。

-編集

おっと、もちろん、関数に割り当てないようにする必要もあります。新しいローカル変数が必要になります。とにかく、今までに誰かがあなたにもっと良い答えをくれたのではないかと思います。


2
それは単に混乱するだけではありません-実に不正確なので、コンパイラはそれを許可しません。
Chii

しかし、必要なときに値を変更するにはどうすればよいですか?
アンクル

混乱するだけでなく、これは、Javaがクロージャをサポートしていないためです。以下の私の答えを参照してください。@Ankur:main()でローカル変数の代わりに、匿名クラスオブジェクトの変数メンバー変数を作成できます。
Jesper、

彼はそれらを修正しているので、それらは最終的なものになることはできません。
ロビン

priceとlastPriceが最終的なものである場合、それらへの割り当てはコンパイルされません。
グレッグマット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.