Java 8 Lambda Mutable Variable Capture aus Methodenparameter?

8

Ich verwende AdoptOpenJDK jdk81212-b04unter Ubuntu Linux unter Eclipse 4.13. Ich habe eine Methode in Swing, die ein Lambda in einem Lambda erzeugt. beide werden wahrscheinlich auf separaten Threads aufgerufen. Es sieht so aus (Pseudocode):

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

Sie können sehen, dass die Lambdas mehrere Schichten tief sind und dass die "Nutzlast", die erfasst wurde, schließlich mehr oder weniger in einer Datei gespeichert wird.

Bevor wir jedoch die Ebenen und Threads betrachten, gehen wir direkt zum Problem:

  1. Beim ersten Aufruf drucken createAction()die beiden System.out.println()Methoden genau denselben Hash-Code, was darauf hinweist, dass die erfasste Nutzlast im Lambda dieselbe ist, an die ich übergeben habe createAction().
  2. Wenn ich später createAction()mit einer anderen Nutzlast anrufe, sind die beiden System.out.println()gedruckten Werte unterschiedlich! Insbesondere zeigt die zweite gedruckte Zeile immer den gleichen Wert an, der in Schritt 1 gedruckt wurde !!

Ich kann das immer und immer wieder wiederholen; Die tatsächlich übergebene Nutzlast erhält immer wieder einen anderen Identitäts-Hash-Code, während die zweite gedruckte Zeile (innerhalb des Lambda) gleich bleibt! Irgendwann wird etwas klicken und plötzlich werden die Zahlen wieder gleich sein, aber dann werden sie mit dem gleichen Verhalten auseinander gehen.

Zwischenspeichert Java irgendwie das Lambda sowie das Argument, das zum Lambda geht? Aber wie ist das möglich? Das payloadArgument ist markiert final, und außerdem müssen Lambda-Erfassungen ohnehin endgültig sein!

  • Gibt es einen Java 8-Fehler, der nicht erkennt, dass ein Lambda nicht zwischengespeichert werden sollte, wenn die erfasste Variable mehrere Lambdas tief ist?
  • Gibt es einen Java 8-Fehler, der Lambdas und Lambda-Argumente über Threads hinweg zwischenspeichert?
  • Oder gibt es etwas, das ich über Argumente der Lambda-Erfassungsmethode im Vergleich zu lokalen Variablen der Methode nicht verstehe?

Erster fehlgeschlagener Workaround-Versuch

Gestern dachte ich, ich könnte dieses Verhalten verhindern, indem ich den Methodenparameter lokal auf dem Methodenstapel erfasse:

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

Data theRealPayload = payloadWenn ich bei dieser einzelnen Zeile fortan theRealPayloadstatt payloadplötzlich den Fehler verwendete, trat er nicht mehr auf, und jedes Mal , wenn ich anrief createAction(), zeigen die beiden gedruckten Zeilen genau dieselbe Instanz der erfassten Variablen an.

Heute funktioniert diese Problemumgehung jedoch nicht mehr.

Separate Fehlerbehebungsadressen Problem; Aber warum?

Ich habe einen separaten Fehler gefunden, der eine Ausnahme auslöste showStatusDialogWithWorker(). Grundsätzlich showStatusDialogWithWorker()soll der Worker (im übergebenen Lambda) erstellt und ein Statusdialogfeld angezeigt werden, bis der Worker fertig ist. Es gab einen Fehler, durch den der Worker korrekt erstellt wurde, der Dialog jedoch nicht erstellt wurde. Dabei wurde eine Ausnahme ausgelöst, die in die Luft sprudelte und niemals abgefangen wurde. Ich habe diesen Fehler behoben, sodass showStatusDialogWithWorker()der Dialog erfolgreich angezeigt wird, wenn der Worker ausgeführt wird, und ihn dann schließt, nachdem der Worker fertig ist. Ich kann das Lambda-Capture-Problem jetzt nicht mehr reproduzieren.

Aber warum bezieht sich etwas im Inneren überhaupt showStatusDialogWithWorker()auf das Problem? Als ich System.identityHashCode()außerhalb und innerhalb des Lambda druckte und die Werte unterschiedlich waren, geschah dies, bevor showStatusDialogWithWorker() aufgerufen wurde und bevor die Ausnahme ausgelöst wurde. Warum sollte eine spätere Ausnahme einen Unterschied machen?

Außerdem bleibt die grundlegende Frage: Wie ist es überhaupt möglich, dass sich ein finalParameter, der von einer Methode übergeben und von einem Lambda erfasst wird, jemals ändern kann?

Garret Wilson
quelle
5
Ich bin mir nicht sicher, ob das ein vollständiges Beispiel ist. Ich würde den Code gerne einfügen und selbst überprüfen, aber angesichts dessen, was Sie geteilt haben, kann ich es einfach nicht. Können Sie ein minimal reproduzierbares Beispiel liefern ?
Fureeish
Ich kann Ihnen im Moment kein minimal reproduzierbares Beispiel geben, aber der Code im Beispiel sollte ausreichen, um die Theorie zu diskutieren. Wie könnte sich aus theoretischer Sicht die Identität eines finalMethodenarguments außerhalb und innerhalb des Lambda ändern? Und warum sollte es finaleinen Unterschied machen , die Variable als lokale Variable auf dem Stapel zu erfassen ?
Garret Wilson
Das heutige Erfassen der Variablen innerhalb der Methode als finalVariable auf dem Stapel behebt das Problem nicht mehr. Ich untersuche weiter.
Garret Wilson
1
In Anbetracht Ihres Anwendungsfalls fällt Ihnen nur die Serialisierung des Lambda ein. das würde erklären, warum sich die Referenz ändert. Ist auch die Nutzlast gleich? Hat es den gleichen Inhalt wie zuvor?
NoDataFound
1
Ich habe den Verdacht, dass der Fehler hier nicht von der Lambda-Gefangennahme herrührt, sondern von sich SwingActionselbst. Was machen Sie mit der SwingActionvon der Methode zurückgegebenen?
Leo Aso

Antworten:

2

Wie ist es überhaupt möglich, dass sich ein endgültiger Parameter, der von einer Methode übergeben und von einem Lambda erfasst wird, jemals ändert?

Es ist nicht. Wie Sie bereits betont haben, kann dies nicht passieren, es sei denn, es liegt ein Fehler in der JVM vor.

Dies ist ohne ein minimal reproduzierbares Beispiel sehr schwer zu bestimmen. Hier sind die Beobachtungen, die Sie gemacht haben:

  1. Wenn ich createAction () zum ersten Mal aufrufe, drucken die beiden System.out.println () -Methoden genau denselben Hash-Code, was darauf hinweist, dass die erfasste Nutzlast im Lambda dieselbe ist, die ich an createAction () übergeben habe.
  2. Wenn ich später createAction () mit einer anderen Nutzlast aufrufe, sind die beiden gedruckten System.out.println () -Werte unterschiedlich! Insbesondere zeigt die zweite gedruckte Zeile immer den gleichen Wert an, der in Schritt 1 gedruckt wurde !!

Eine mögliche Erklärung, die zum Beweis passt, ist, dass das Lambda, das zum zweiten Mal aufgerufen wird, tatsächlich das Lambda aus dem ersten Lauf ist und das Lambda, das durch den zweiten Lauf erzeugt wurde, verworfen wurde. Das würde die obigen Beobachtungen geben und den Fehler in den Code einfügen, den Sie hier nicht gezeigt haben.

Vielleicht könnten Sie zusätzliche Protokollierung hinzufügen, um Folgendes aufzuzeichnen: a) die IDs aller Lambdas, die zum Zeitpunkt der Erstellung in createAction erstellt wurden (ich denke, Sie müssten die Lambdas in anon-Klassen ändern, die die Rückrufschnittstellen mit der Protokollierung in ihren Konstruktoren implementieren) b) die IDs der Lambdas zum Zeitpunkt ihrer Anrufung

Ich denke, dass die obige Protokollierung ausreichen würde, um meine Theorie zu beweisen oder zu widerlegen.

GL!

Reich
quelle
0

Ich finde nichts falsch mit den gefangenen Lambdas in Ihrem Code.

Ihre Problemumgehung ändert nichts an der lokalen Erfassung, da Sie gerade eine neue Variable deklariert und derselben Referenz zugewiesen haben.

Das Problem liegt höchstwahrscheinlich in Ihrer Handhabung des SwingActionerstellten Objekts. Es würde mich nicht wundern, wenn Sie feststellen, dass das Drucken des IdentityHashCodes des zurückgegebenen Objekts, in dem Sie es verwenden, konsistente Werte mit Ihren Nutzdaten liefert. Mit anderen Worten, Sie verwenden möglicherweise einen vorherigen Verweis auf SwingAction.

Außerdem bleibt die grundlegende Frage: Wie ist es überhaupt möglich, dass sich ein endgültiger Parameter, der von einer Methode übergeben und von einem Lambda erfasst wird, jemals ändern kann?

Dies sollte auf der Referenzebene nicht möglich sein, die Variable kann nicht neu zugewiesen werden. Die übergebene Referenz kann zwar selbst veränderbar sein, dies gilt jedoch nicht für diesen Fall.

Tomas Fornara
quelle