web-dev-qa-db-ja.com

Java "空白の最終フィールドが初期化されていない可能性があります"匿名インターフェースとラムダ式

最近、「空白の最終フィールドobjが初期化されていない可能性があります」というエラーメッセージが表示されました。

通常、これは、まだ値が割り当てられていない可能性のあるフィールドを参照しようとした場合です。クラスの例:

_public class Foo {
    private final Object obj;
    public Foo() {
        obj.toString(); // error           (1)
        obj = new Object();
        obj.toString(); // just fine       (2)
    }
}
_

私はEclipseを使用しています。 _(1)_行でエラーが発生し、_(2)_行ですべてが機能します。 これまでのところ意味があります。

次に、コンストラクター内で作成した匿名インターフェイス内でobjにアクセスしようとします。

_public class Foo {
    private Object obj;
    public Foo() {
        Runnable run = new Runnable() {
            public void run() {
                obj.toString(); // works fine
            }
        };
        obj = new Object();
        obj.toString(); // works too
    }
}
_

これも機能します。インターフェイスを作成した瞬間はobjにアクセスしないためです。また、インスタンスを別の場所に渡し、オブジェクトobjを初期化して、インターフェイスを実行することもできます。 (ただし、使用する前にnullを確認することをお勧めします)。 それでも意味があります。

しかし、今ではRunnableインスタンスの作成をラムダ式を使用してハンバーガー矢印バージョンに短縮しています:

_public class Foo {
    private final Object obj;
    public Foo() {
        Runnable run = () -> {
            obj.toString(); // error
        };
        obj = new Object();
        obj.toString(); // works again
    }
}
_

そして、ここで私はもう追跡できません。ここで再び警告が表示されます。コンパイラーがラムダ式を通常の初期化として処理しないことを知っています。「長いバージョンに置き換えられません」。しかし、これがRunnableオブジェクトの作成時にrun()メソッドのコード部分を実行しないという事実に影響するのはなぜですか?私はまだ初期化を行うことができますbeforerun()を呼び出します。したがって、技術的には、ここでNullPointerExceptionに遭遇しないようにすることができます。 (ここでもnullを確認することをお勧めしますが、この規則は別のトピックです。)

私が犯す間違いは何ですか?ラムダについて、オブジェクトの使用に影響を与えるほど異なる方法で処理されるものは何ですか?

それ以上の説明に感謝します。

21
Steffen T

私はあなたの最後のケースのエラーをEclipseのコンパイラーで再現できません。

ただし、私が想像できるOracleコンパイラの推論は次のとおりです。ラムダ内では、objの値を宣言時にキャプチャする必要があります。つまり、ラムダ本体内で宣言するときに初期化する必要があります。

ただし、この場合、JavaはFooではなくobjインスタンスの値をキャプチャする必要があります。その後、objにアクセスできます。 (初期化済み)Fooオブジェクト参照とそのメソッドの呼び出しこれは、Eclipseコンパイラーがコードをコンパイルする方法です。

これは、仕様 here で示唆されています。

メソッド参照式の評価のタイミングは、ラムダ式のタイミングよりも複雑です(§15.27.4)。メソッド参照式に::区切り記号の前に(タイプではなく)式がある場合、その部分式はすぐに評価されます。 評価の結果は、対応する機能インターフェースタイプのメソッドが呼び出されるまで保存されます;その時点で、結果は呼び出しのターゲット参照として使用されます。つまり、::区切り記号の前の式は、プログラムがメソッド参照式に遭遇した場合にのみ評価され、機能インターフェースタイプの後続の呼び出しでは再評価されません。

同様のことが起こります

_Object obj = new Object(); // imagine some local variable
Runnable run = () -> {
    obj.toString(); 
};
_

objがローカル変数であると想像してください。ラムダ式コードが実行されると、objが評価され、参照が生成されます。この参照は、作成されたRunnableインスタンスのフィールドに格納されます。 run.run()が呼び出されると、インスタンスは保存されている参照値を使用します。

これは、objが初期化されていない場合は発生しません。例えば

_Object obj; // imagine some local variable
Runnable run = () -> {
    obj.toString(); // error
};
_

ラムダはまだ値がないため、objの値をキャプチャできません。それは事実上同等です

_final Object anonymous = obj; // won't work if obj isn't initialized
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Object val) {
        this.someHiddenRef = val;
    }
    private final Object someHiddenRef;
    public void run() {
        someHiddenRef.toString(); 
    }
}
_

これは、Oracleコンパイラが現在スニペットに対して動作している方法です。

ただし、Eclipseコンパイラーは、代わりにobjの値をキャプチャしていません。thisFooインスタンス)の値をキャプチャしています。それは事実上同等です

_final Foo anonymous = Foo.this; // you're in the Foo constructor so this is valid reference to a Foo instance
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Foo foo) {
        this.someHiddenRef = foo;
    }
    private final Foo someHiddenFoo;
    public void run() {
        someHiddenFoo.obj.toString(); 
    }
}
_

Fooが呼び出されるときまでにrunインスタンスが完全に初期化されていると想定しているので、これは問題ありません。

11

あなたは問題を回避することができます

        Runnable run = () -> {
            (this).obj.toString(); 
        };

これはラムダ開発中に議論されました、基本的にラムダ本体は明確な代入分析中にローカルコードとして扱われます。

Dan Smith、spec tzar、 https://bugs.openjdk.Java.net/browse/JDK-8024809 の引用

ルールは2つの例外を切り分けます:... ii)匿名クラスの内部からの使用は問題ありません。ラムダ式内での使用に例外はありません

率直に言って、私と他の何人かの人々は決定が間違っていると思いました。ラムダはthisのみをキャプチャし、objはキャプチャしません。このケースは、匿名クラスと同じように扱われるべきでした。現在の動作は、多くの正当なユースケースにとって問題です。さて、あなたはいつも上記のトリックを使ってそれをバイパスすることができます-幸いにも明確な代入分析はあまりスマートではなく、私たちはそれをだますことができます。

16
ZhongYu

ユーティリティメソッドを使用すると、thisのみを強制的にキャプチャできます。これはJava 9でも動作します。

public static <T> T r(T object) {
    return object;
}

これで、ラムダを次のように書き換えることができます。

Runnable run = () -> r(this).obj.toString();
0
Dávid Horváth