メソッドパラメーターからのJava 8ラムダ可変変数キャプチャ?


8

私はjdk81212-b04、Eclipse 4.13で実行されているUbuntu LinuxでAdoptOpenJDK を使用しています。Swingにはラムダ内にラムダを作成するメソッドがあります。おそらく両方とも別々のスレッドで呼び出されます。これは次のようになります(疑似コード):

private SwingAction createAction(final Data payload) {
  System.out.println(System.identityHashCode(payload));
  return new SwingAction(() -> {
    System.out.println(System.identityHashCode(payload));
    //do stuff
    //show an "are you sure?" modal dialog and get a response
    //show a file selection dialog
    //when the dialog completes, create a worker and show a status:
    showStatusDialogWithWorker(() -> new SwingWorker() {
      protected String doInBackground() {
        save(payload);
      }
    });

ラムダは数層の深さであり、最終的にはキャプチャされた「ペイロード」がファイルに保存されることがわかります。

しかし、レイヤーとスレッドを検討する前に、問題に直接移動しましょう。

  1. 初めて呼び出したcreateAction()とき、2つのSystem.out.println()メソッドはまったく同じハッシュコードを出力しcreateAction()ます。これは、ラムダ内でキャプチャされたペイロードがに渡したものと同じであることを示しています。
  2. 後でcreateAction()別のペイロードで呼び出すと、出力System.out.println()される2つの値が異なります!特に、印刷された2行目は、常にステップ1で印刷されたものと同じ値を示しています。

私はこれを何度も繰り返すことができます。渡された実際のペイロードは異なるIDハッシュコードを取得し続けますが、(ラムダ内に)印刷された2行目は同じままです!最終的に何かがクリックされ、突然数は再び同じになりますが、その後同じ動作で分岐します。

Javaはどういうわけかラムダとラムダに行く引数をキャッシュしていますか?しかし、これはどのように可能ですか?payload引数がマークされfinal、しかも、ラムダキャプチャは効果的に最終とにかくする必要が!

  • キャプチャされた変数が数ラムダの深さである場合、ラムダがキャッシュされるべきでないことを認識しないJava 8バグはありますか?
  • スレッド全体でラムダとラムダ引数をキャッシュするJava 8バグはありますか?
  • または、ラムダキャプチャメソッドの引数とメソッドローカル変数について理解できないことはありますか?

最初に失敗した回避策の試み

昨日、メソッドスタックでメソッドパラメータをローカルにキャプチャするだけでこの動作を防ぐことができると思いました。

private SwingAction createAction(final Data payload) {
  final Data theRealPayload = payload;
  System.out.println(System.identityHashCode(theRealPayload));
  return new SwingAction(() -> {
    System.out.println(System.identityHashCode(theRealPayload));
    //do stuff
    //show an "are you sure?" modal dialog and get a response
    //show a file selection dialog
    //when the dialog completes, create a worker and show a status:
    showStatusDialogWithWorker(() -> new SwingWorker() {
      protected String doInBackground() {
        save(theRealPayload);
      }
    });

その1行でData theRealPayload = payload、突然ではtheRealPayloadなく今後使用した場合はpayload、バグは発生しなくなりましcreateAction()た。1 回呼び出すたびに、2つの出力行は、キャプチャされた変数のまったく同じインスタンスを示しています。

しかし、今日、この回避策は機能しなくなりました。

個別のバグ修正で問題に対処。しかし、なぜ?

内部で例外をスローしていた別のバグを見つけましたshowStatusDialogWithWorker()。基本的にshowStatusDialogWithWorker()は(渡されたラムダで)ワーカーを作成し、ワーカーが完了するまでステータスダイアログを表示することになっています。ワーカーを正しく作成するが、ダイアログの作成に失敗し、バブルアップしてキャッチされない例外をスローするバグがありました。このバグを修正して、showStatusDialogWithWorker()ワーカーが実行されているときにダイアログが正常に表示され、ワー​​カーが終了したらダイアログを閉じるようにしました。ラムダキャプチャの問題を再現できなくなりました。

しかし、なぜ内部の何かshowStatusDialogWithWorker()が問題に関係しているのでしょうか?私がプリントアウトされたときにSystem.identityHashCode()外側と内側ラムダ、および値が異なった、これが起こっていたの前に showStatusDialogWithWorker()呼ばれていた、と例外の前に送出されていました。後の例外で違いが生じるのはなぜですか?

その上、根本的な質問が残っています:finalメソッドによって渡され、ラムダによってキャプチャされたパラメーターが変更される可能性はありますか?


5
それが完全な例かどうかはわかりません。コードを貼り付けて自分で検査したいのですが、あなたが共有した内容を考えると、私はできません。最小限の再現可能な例を提供できますか?
フルイッシュ

現時点では再現可能な最小限の例を提供することはできませんが、例のコードは理論を説明するのに十分なはずです。理論的な観点から、finalメソッドの引数のアイデンティティはラムダの外側と内側でどのように変化するでしょうか?また、変数をfinalスタック上のローカル変数としてキャプチャすると、なぜ違いが生じるのでしょうか。
Garret Wilson、

現在、メソッド内のfinal変数をスタック上の変数としてキャプチャしても、問題は修正されなくなりました。私は調査を続けています。
Garret Wilson、

1
ユースケースを考えると、頭に浮かぶのはラムダのシリアル化だけです。これにより、参照が変更される理由が説明されます。また、ペイロードは等しいですか?以前と同じ内容ですか?
NoDataFound

1
ここのバグはラムダキャプチャーからではなく、SwingActionそれ自体から来ているのではないかと疑いを抱いています。SwingActionメソッドから返されたものをどうしますか?
Leo Aso

回答:


2

メソッドによって渡され、ラムダによってキャプチャされた最終的なパラメーターが変更される可能性さえあるのでしょうか。

そうではない。ご指摘のとおり、JVMにバグがない限り、これは発生しません。

これは、最小限の再現可能な例がなければ、特定するのが非常に困難です。ここにあなたがした観察があります:

  1. 最初にcreateAction()を呼び出したとき、2つのSystem.out.println()メソッドはまったく同じハッシュコードを出力します。これは、ラムダ内でキャプチャされたペイロードがcreateAction()に渡したものと同じであることを示しています。
  2. 後で別のペイロードでcreateAction()を呼び出すと、出力される2つのSystem.out.println()値が異なります。特に、印刷された2行目は常に、手順1で印刷されたものと同じ値を示しています。

証拠に当てはまる1つの考えられる説明は、2回目に呼び出されるラムダは実際には1回目の実行からのラムダであり、2回目の実行によって作成されたラムダは破棄されていることです。これにより、上記の観察結果が得られ、ここに示していないバグがコード内に配置されます。

おそらく、追加のログを記録に追加できます。呼び出し時のラムダのID

上記の検層は私の理論を証明または反証するには十分だと思います。

GL!


0

コード内のキャプチャされたラムダに問題はありません。

同じ参照に割り当てられた新しい変数を宣言しただけなので、回避策はローカルキャプチャを変更しません。

問題は、SwingAction作成されたオブジェクトの処理にある可能性が高いです。返されたオブジェクトを使用している場所でIdentityHashCodeを出力すると、ペイロードと一貫した値が得られても驚くことはありません。あるいは、以前のへの参照を使用している可能性がありますSwingAction

その上、根本的な質問が残っています。メソッドによって渡され、ラムダによってキャプチャされた最終的なパラメータが変更される可能性はありますか?

これは参照レベルでは不可能であるべきであり、変数を再割り当てすることはできません。渡された参照自体は変更可能かもしれませんが、それはこの場合には当てはまりません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.