web-dev-qa-db-ja.com

無名クラスで最後の変数だけがアクセス可能なのはなぜですか?

  1. aはここで最終的なものにすることができます。どうして?プライベートメンバとして保持せずにonClick()メソッドでaを再割り当てする方法を教えてください。

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
  2. クリックしたときにどうすれば5 * aを返すことができますか?というのは、

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }
    
330
user467871

コメントで述べたように、これのいくつかはfinalが暗黙的になる可能性があるJava 8では無関係になります。ただし、無名の内部クラスまたはラムダ式では、事実上の最終変数しか使用できません。


これは基本的にはJavaが管理する方法によるものです クロージャ

無名の内部クラスのインスタンスを作成すると、そのクラス内で使用されている変数はすべて、自動生成されたコンストラクタを介してコピーされたを持ちます。これにより、たとえばC#コンパイラが行うように、コンパイラが "ローカル変数"の論理状態を保持するためにさまざまな追加の型を自動生成する必要がなくなります(C#が無名関数で変数をキャプチャするとき、実際に変数をキャプチャします。クロージャーは、メソッドの本体から見た方法で変数を更新できます。逆も同様です。

値が匿名の内部クラスのインスタンスにコピーされているので、変数がメソッドの残りの部分によって変更される可能性がある場合は奇妙に見えます - 古い変数で動作しているように見えるコードがあります(なぜならそれは事実上起こっていることです...あなたは別の時に撮られたコピーで作業しているでしょう)。同様に、匿名内部クラス内で変更を加えることができれば、開発者はそれらの変更がそれを囲むメソッドの本体内に表示されると期待するかもしれません。

変数をfinalにすると、これらすべての可能性がなくなります。値はまったく変更できないため、そのような変更が表示されるかどうかを心配する必要はありません。メソッドと匿名の内部クラスが互いの変更を認識できるようにする唯一の方法は、何らかの記述の可変型を使用することです。これを囲むクラス自体、配列、可変ラッパー型などが考えられます。基本的には、あるメソッドと別のメソッド間の通信に少し似ています。あるメソッドのパラメータに加えられた変更は、呼び出し元には見えませんが、参照されたオブジェクトに加えられた変更パラメータによるまでが見られます。

JavaとC#のクロージャーのより詳細な比較に興味があるなら、私は article を持っています。私はこの答えでJava側に集中したかったです:)

463
Jon Skeet

無名クラスが外部スコープのデータを更新するのを可能にするトリックがあります。

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

しかし、このトリックは同期の問題のためあまり良くありません。後でhandlerが呼び出された場合は、1)別のスレッドからhandlerが呼び出された場合はresにアクセスを同期させる必要があります。2)resが更新されたことを示すフラグまたは指示が必要です。

ただし、匿名クラスが同じスレッド内ですぐに呼び出される場合は、この方法でうまくいきます。好きです:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...
42
Ivan Dubrov

無名クラスは内部クラスであり、厳密な規則は内部クラスに適用されます(JLS 8.1.3)

使用されているが内部クラスで宣言されていないローカル変数、仮メソッド・パラメーター、または例外ハンドラー・パラメーターは、最終宣言でなければなりません。内部クラスで使用されているが宣言されていないローカル変数は、必ず内部クラスの本体の前に代入する必要があります。

Jlsやjvmsに関する理由や説明はまだ見つかっていませんが、コンパイラが内部クラスごとに別々のクラスファイルを作成し、このクラスファイルでメソッドが宣言されていることを確認する必要があります。バイトコードレベルでは)少なくともローカル変数の値にアクセスできます。

Jonは完全な答えを持っています - JLSのルールに興味があるかもしれないので、これを元に戻していません)

16
Andreas_D

戻り値を取得するためにクラスレベルの変数を作成できます。というのは

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

今、あなたはKの値を得て、あなたが望むところでそれを使うことができます。

なぜあなたの答えは:

ローカル内部クラスのインスタンスはMainクラスに結び付けられていて、それを含むメソッドの最後のローカル変数にアクセスできます。インスタンスがその包含メソッドの最終ローカルを使用すると、その変数が範囲外になった場合でも、変数はインスタンスの作成時に保持していた値を保持します(これは事実上Javaの粗い制限バージョンのクロージャです)。

ローカル内部クラスはクラスまたはパッケージのメンバーではないため、アクセスレベルで宣言されていません。 (ただし、通常のクラスと同じように、そのメンバーにはアクセスレベルがあります。)

10
Ashfak Balooch

Javaでは、変数は単なるパラメーターとしてではなく、クラスレベルのフィールドとしての最終的なものになることがあります。

public class Test
{
 public final int a = 3;

またはローカル変数として、

public static void main(String[] args)
{
 final int a = 3;

無名クラスから変数にアクセスして変更したい場合は、 を囲む クラス内で変数をa class-level 変数にすることをお勧めします。

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

Final および のように変数に新しい値を与えることはできません。 finalは次のことを意味します。値は変更不可能で最終的なものです。

そして最後になったので、Javaは安全に copy をローカルの無名クラスにコピーすることができます。 intへの参照を取得していません(特にJavaのintのようなプリミティブへの参照はできないため、 への参照のみ)。オブジェクト )。

それは単にaの値をあなたの匿名クラスのaと呼ばれる暗黙のintにコピーするだけです。

6
Zach L

アクセスがローカルの最終変数のみに制限されているのは、すべてのローカル変数がアクセス可能になる場合、まず内部クラスがアクセスできる別のセクションにコピーし、複数のコピーを維持する必要があるためです。可変ローカル変数は、データの不整合を招く可能性があります。一方、最終変数は不変なので、それらへのコピーがいくらでもデータの整合性に影響を与えることはありません。

6

無名の内部クラスがメソッドの本体内で定義されている場合、そのメソッドのスコープ内でfinalと宣言されたすべての変数は、内部クラス内からアクセスできます。スカラー値の場合、いったん割り当てられると、最後の変数の値は変更できません。オブジェクト値の場合、参照は変更できません。これにより、Javaコンパイラーは実行時に変数の値を「キャプチャー」し、そのコピーを内部クラスのフィールドとして保管することができます。外部メソッドが終了してそのスタックフレームが削除されると、元の変数は消えますが、内部クラスのプライベートコピーはクラス自身のメモリ内に残ります。

http://en.wikipedia.org/wiki/Final_%28Java%29

2
ola_star

アノノミーな内部クラス内のメソッドは、それを生成したスレッドが終了した後で起動されることがあります。あなたの例では、内部クラスはそれを作成したものと同じスレッドではなくイベントディスパッチスレッドで呼び出されるでしょう。したがって、変数の範囲は異なります。そのため、そのような変数代入スコープの問題を保護するために、あなたはそれらをfinalと宣言しなければなりません。

2
S73417H
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}
1
Bruce Zu

この制限の理論的根拠を理解するために、以下のプログラムを考えてください。

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

initializeメソッドが返された後、interfaceInstanceはメモリに残りますが、パラメータvalは戻りません。 JVMはその範囲外のローカル変数にアクセスできないため、その後のprintIntegerの呼び出しは、同じ名前の暗黙のフィールドにvalの値をコピーすることによって機能します。 within interfaceInstanceinterfaceInstanceはローカルパラメータの値キャプチャを持つと言われています。パラメータが最終的(または実質的に最終的)に変更されず、キャプチャされた値と同期しなくなり、直感に反する動作が生じる可能性があります。

このコードを試してください、

配列リストを作成し、その中に値を入れて返します。

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}
0
Wasim

Jon が実装の詳細な答えを持っているので、他の可能な答えはJVMが彼のアクティベーションを終了したレコードの書き込みを処理したくないということでしょう。

ラムダが適用されるのではなく、ある場所に格納されて後で実行されるユースケースを考えてみましょう。

Smalltalkでは、あなたがそのような修正をすると違法な店を設立することを覚えています。

0
mathk