Ich verwende AdoptOpenJDK jdk81212-b04
unter 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:
- Beim ersten Aufruf drucken
createAction()
die beidenSystem.out.println()
Methoden genau denselben Hash-Code, was darauf hinweist, dass die erfasste Nutzlast im Lambda dieselbe ist, an die ich übergeben habecreateAction()
. - Wenn ich später
createAction()
mit einer anderen Nutzlast anrufe, sind die beidenSystem.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 payload
Argument 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 = payload
Wenn ich bei dieser einzelnen Zeile fortan theRealPayload
statt payload
plö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 final
Parameter, der von einer Methode übergeben und von einem Lambda erfasst wird, jemals ändern kann?
final
Methodenarguments außerhalb und innerhalb des Lambda ändern? Und warum sollte esfinal
einen Unterschied machen , die Variable als lokale Variable auf dem Stapel zu erfassen ?final
Variable auf dem Stapel behebt das Problem nicht mehr. Ich untersuche weiter.SwingAction
selbst. Was machen Sie mit derSwingAction
von der Methode zurückgegebenen?Antworten:
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:
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!
quelle
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
SwingAction
erstellten 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 aufSwingAction
.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.
quelle