匿名クラスでアクセスできるのは最終的な変数だけなのはなぜですか?


355
  1. aここでのみ最終的にすることができます。どうして?プライベートメンバーとして保持せずaonClick()メソッドに再割り当てするにはどうすればよいですか?

    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 
            }
        });
    }

1
Javaの匿名クラスが期待どおりのラムダクロージャを提供するとは思わないが、私が間違っている場合は誰かが私を修正してください...
user541686

4
何を達成しようとしていますか?「f」が終了すると、クリックハンドラが実行される可能性があります。
Ivan Dubrov、2011年

@LambertをonClickメソッドで使用する場合、それは最終的なものである必要があります@Ivan f()メソッドがどのように動作して、onClick()メソッドがクリックされたときにintを返すか

2
それが私の言いたいことです-非ファイナル変数へのアクセスを許可しないため、完全なクロージャーをサポートしていません。
user541686 '19年

4
注:Java 8以降、変数は事実上最終的なものである
Peter Lawrey '24

回答:


489

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


これは基本的に、Javaがクロージャを管理する方法が原因です。

匿名の内部クラスのインスタンスを作成すると、そのクラス内で使用されるすべての変数の値が、自動生成されたコンストラクターを介してコピーされます。これにより、C#コンパイラが行うように、コンパイラが「ローカル変数」の論理状態を保持するためにさまざまな追加の型を自動生成する必要がなくなります...クロージャーは、メソッドの本体で認識される方法で変数を更新できます(逆も同様です)。

値が匿名の内部クラスのインスタンスにコピーされているので、メソッドの残りの部分で変数を変更できる場合は奇妙に見えます。古い変数で動作しているように見えるコードがある可能性があります(それが効果的に何だからでしょう起きて...あなたは、異なる時間で採取されたコピー)と仕事ができると思います。同様に、匿名の内部クラス内で変更を加えることができる場合、開発者はそれらの変更が外側のメソッドの本体内で可視になることを期待するかもしれません。

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

JavaクロージャーとC#クロージャーのより詳細な比較に興味がある場合は、さらに詳しく説明している記事があります。私はこの答えでJava側に焦点を当てたかった:)


4
@Ivan:基本的にC#と同じです。ただし、さまざまなスコープの変数をさまざまな回数「インスタンス化」できるC#と同じ種類の機能が必要な場合は、かなり複雑になります。
Jon Skeet、2011年


11
これはJava 7にすべて当てはまりました。Java8ではクロージャが導入され、クラスの非finalフィールドにその内部クラスからアクセスできるようになったことを覚えておいてください。
Mathias Bader

22
@MathiasBader:本当に?それは本質的に同じメカニズムであると私は思いました、コンパイラーは推論するのに十分賢いですfinal(しかしそれはまだ効果的に最終である必要があります)。
Thilo

3
@Mathias Bader:常にfinal以外のフィールドにアクセスできます。これは、ローカル変数と混同しないでください。ローカル変数はfinalでなければならず、事実上finalでなければならないため、Java 8はセマンティクスを変更しません。
Holger

41

匿名クラスが外部スコープのデータを更新できるようにするトリックがあります。

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?
}

ただし、同期の問題のため、このトリックはあまり良くありません。ハンドラーが後で呼び出された場合、1)ハンドラーが別のスレッドから呼び出された場合、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]);

// ...

2
ご回答有難うございます。私はこれをすべて知っており、私の解決策はこれより優れています。私の質問は「なぜ最終的なのか」です。

6
その答えは、それらがどのように実装されるかということです:)
Ivan Dubrov

1
ありがとう。私は自分で上記のトリックを使いました。それが良い考えかどうかはわかりませんでした。Javaがそれを許可しない場合は、正当な理由がある可能性があります。あなたの答えは私のList.forEachコードが安全であることを明確にします。
2015年

「なぜ最終のみ」の背後にある理論的根拠についての良い議論については、stackoverflow.com / q / 12830611/2073130を読んでください。
lcn

いくつかの回避策があります。鉱山は:final int resf = res; 最初は配列アプローチを使用しましたが、構文が面倒すぎることがわかりました。AtomicReferenceは少し遅いかもしれません(オブジェクトを割り当てます)。
zakmck 2017年

17

匿名クラスは内部クラスであり、厳格なルールは内部クラスに 適用されます(JLS 8.1.3)

使用されているが内部クラスで宣言されていないローカル変数、正式メソッドパラメータ、または例外ハンドラパラメータはfinalで宣言する必要があります。内部クラスで使用されているが宣言されていないローカル変数は、必ず内部クラスの本体の前に割り当てる必要があります

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

ジョンは完全な答えを持っています-JLSルールに興味があるかもしれないので、私はこれを削除せずに保ちます)


11

クラスレベルの変数を作成して、戻り値を取得できます。というのは

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の粗雑な限定バージョンのクロージャーです)。

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


私は、「プライベートメンバーとして、それを維持することなく、」それを言及

6

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

public class Test
{
 public final int a = 3;

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

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

匿名クラスの変数にアクセスして変更する場合は、その変数を、囲んでいるクラスのクラスレベルの変数にすることができます。

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はローカルの匿名クラスに安全にコピーできます。intへの参照を取得していません(特に、Javaのintのようなプリミティブへの参照はできず、単にObjectへの参照があるためです)。

これは、aの値を、匿名クラスのaと呼ばれる暗黙のintにコピーするだけです。


3
「クラスレベル変数」をに関連付けます static。代わりに「インスタンス変数」を使用すると、より明確になる場合があります。
eljenso 2011年

1
まあ、この手法はインスタンス変数と静的変数の両方で機能するため、クラスレベルを使用しました。
Zach L

ファイナルがアクセス可能であることはすでに知っていますが、なぜか知りたいのですか?理由についてもう少し説明をお願いします。
Saurabh Oza

6

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


これは、この機能をサポートするC#などの言語での実装方法ではありません。実際、コンパイラーは変数をローカル変数からインスタンス変数に変更するか、外部クラスのスコープを超えて存続する可能性のあるこれらの変数の追加のデータ構造を作成します。ただし、「ローカル変数の複数のコピー」はありません
Mike76 '27

Mike76私はC#の実装を見て持っていなかったが、Scalaは、あなたは私が考える二つ目言及しない:場合はIntクロージャ内に再割り当てされている、変更をそのインスタンスに変数IntRef(基本的に変更可能なIntegerラッパー)。その後、すべての変数アクセスがそれに応じて書き換えられます。
Adowrath 2017

3

この制限の根拠を理解するには、次のプログラムを検討してください。

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();
    }
}

interfaceInstanceは、後にメモリに残って初期化方法を返しますが、パラメータvalがありません。JVMはスコープ外のローカル変数にアクセスできないため、Javaは、valの値をinterfaceInstance内の同じ名前の暗黙的なフィールドにコピーすることにより、後続のprintIntegerの呼び出しを機能させますinterfaceInstanceをしていると言われているキャプチャローカルパラメータの値を。パラメータが最終的でない(または実質的に最終的でない)場合、その値が変化し、キャプチャされた値と同期しなくなり、直感的でない動作を引き起こす可能性があります。


2

匿名の内部クラス内のメソッドは、それを生成したスレッドが終了した後で十分に呼び出すことができます。あなたの例では、内部クラスはそれを作成したスレッドと同じスレッドではなく、イベントディスパッチスレッドで呼び出されます。したがって、変数のスコープは異なります。したがって、このような変数割り当てスコープの問題を保護するには、それらを最終的に宣言する必要があります。


2

匿名の内部クラスがメソッドの本体内で定義されている場合、そのメソッドのスコープでfinalと宣言されているすべての変数は、内部クラス内からアクセスできます。スカラー値の場合、一度割り当てられると、最終的な変数の値は変更できません。オブジェクト値の場合、参照は変更できません。これにより、Javaコンパイラーは実行時に変数の値を「取り込み」、コピーをフィールドとして内部クラスに格納できます。外部メソッドが終了し、そのスタックフレームが削除されると、元の変数は失われますが、内部クラスのプライベートコピーはクラス自体のメモリに保持されます。

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

0

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

ラムダが適用されるのではなく、どこかに保存され、後で実行されるユースケースを考えてみましょう。

Smalltalkでは、そのような変更を行うと、違法ストアが発生することを覚えています。


0

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

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

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;
}

OPは、なぜ何かが必要なのかについて理由を尋ねています。したがって、コードがそれをどのように処理しているかを指摘する必要があります
NitinSingh

0

Javaの無名クラスはJavaScriptのクロージャと非常に似ていますが、Javaはそれを異なる方法で実装します。(アンデルセンの答えを確認してください)

そのため、Java開発者を、JavaScriptのバックグラウンドから来た人に起こり得る奇妙な動作と混同しないようにしてください。私が使用を強制するのはそのためだと思いますがfinal、これはJVMの制限ではありません。

以下のJavascriptの例を見てみましょう。

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

JavaScriptでは、counter値が1つしかないため、値は100になります。counter最初から最後まで変数。

しかし、Javaでは、 final、それはアウト印刷されません0内部オブジェクトが作成されている間、ので、0値は内部クラスのオブジェクトの隠されたプロパティにコピーされます。(ここには2つの整数変数があり、1つはローカルメソッドにあり、もう1つは内部クラスの非表示プロパティにあります)

したがって、内部オブジェクトの作成後の変更(1行目など)は、内部オブジェクトには影響しません。そのため、2つの異なる結果と動作(JavaとJavaScriptの間)が混同されます。

それがJavaが最終的なものになることを強制することを決定した理由だと私は信じています。そのため、データは最初から最後まで「一貫しています」。


0

内部クラス内のJava最終変数

内部クラスのみ使用できます

  1. 外部クラスからの参照
  2. 参照型であるスコープ外の最終ローカル変数(例:Object...)
  3. value(primitive)(eg int...)型は、最終参照型でラップできます。IntelliJ IDEAそれを1つの要素配列に変換するのに役立ちます

non static nestedinner class[About]がコンパイラーによって生成されたとき-新しいクラス<OuterClass>$<InnerClass>.classが作成され、バインドされたパラメーターがコンストラクターに渡されます[スタックのローカル変数]。閉鎖に似ています

final変数は、再割り当てできない変数です。最終的な参照変数は、状態を変更することで変更できます

プログラマーとしてあなたがこのように作ることができたのでそれは奇妙である可能性がありました

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}

-2

多分このトリックはあなたにアイデアを与えます

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.