¿Java 8 captura variable variable lambda del parámetro del método?

8

Estoy usando AdoptOpenJDK jdk81212-b04en Ubuntu Linux, ejecutándome en Eclipse 4.13. Tengo un método en Swing que crea una lambda dentro de una lambda; ambos probablemente sean llamados en hilos separados. Se ve así (pseudocódigo):

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

Puede ver que las lambdas tienen varias capas de profundidad y que, finalmente, la "carga útil" capturada se guarda en un archivo, más o menos.

Pero antes de considerar las capas y los hilos, vayamos directamente al problema:

  1. La primera vez que llamo createAction(), los dos System.out.println()métodos imprimen exactamente el mismo código hash, lo que indica que la carga útil capturada dentro de la lambda es la misma que le pasé createAction().
  2. Si luego llamo createAction()con una carga útil diferente, ¡los dos System.out.println()valores impresos son diferentes! ¡En particular, la segunda línea impresa siempre indica el mismo valor que se imprimió en el paso 1!

Puedo repetir esto una y otra vez; ¡la carga útil real pasada seguirá obteniendo un código hash de identidad diferente, mientras que la segunda línea impresa (dentro de la lambda) permanecerá igual! Finalmente, algo hará clic y de repente los números volverán a ser los mismos, pero luego divergirán con el mismo comportamiento.

¿De alguna manera Java está almacenando en caché la lambda, así como el argumento que va a la lambda? Pero, ¿cómo es esto posible? El payloadargumento está marcado final, y además, ¡las capturas lambda deben ser efectivamente finales de todos modos!

  • ¿Existe un error de Java 8 que no reconoce un lambda que no debe almacenarse en caché si la variable capturada tiene varias lambdas de profundidad?
  • ¿Hay un error de Java 8 que está almacenando en caché lambdas y argumentos lambda a través de hilos?
  • ¿O hay algo que no entiendo sobre los argumentos del método de captura lambda versus las variables locales del método?

Primer intento fallido de solución

Ayer pensé que podría evitar este comportamiento simplemente capturando el parámetro del método localmente en la pila de métodos:

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

Con esa sola línea Data theRealPayload = payload, si en adelante utilicé en theRealPayloadlugar de payloadrepentinamente el error ya no aparecía, y cada vez que llamé createAction(), las dos líneas impresas indican exactamente la misma instancia de la variable capturada.

Sin embargo, hoy esta solución ha dejado de funcionar.

Problema de direcciones de corrección de errores por separado; ¿Pero por qué?

Encontré un error separado que arrojaba una excepción dentro showStatusDialogWithWorker(). Básicamente showStatusDialogWithWorker()se supone que crea el trabajador (en la lambda aprobada) y muestra un cuadro de diálogo de estado hasta que el trabajador haya terminado. Hubo un error que crearía al trabajador correctamente, pero no pudo crear el diálogo, lanzando una excepción que surgiría y nunca quedaría atrapada. Solucioné este error para que showStatusDialogWithWorker()muestre con éxito el cuadro de diálogo cuando el trabajador se está ejecutando y luego lo cierra una vez que el trabajador termina. Ahora ya no puedo reproducir el problema de captura lambda.

Pero, ¿por qué algo dentro se showStatusDialogWithWorker()relaciona con el problema? Cuando imprimía System.identityHashCode()fuera y dentro de la lambda, y los valores eran diferentes, esto sucedía antes de que showStatusDialogWithWorker() se llamara y antes de que se lanzara la excepción. ¿Por qué una excepción posterior debería marcar la diferencia?

Además, la pregunta fundamental sigue siendo: ¿cómo es posible que un finalparámetro pasado por un método y capturado por una lambda pueda cambiar?

Garret Wilson
fuente
55
No estoy seguro de si ese es un ejemplo completo . Me encantaría pegar el código e inspeccionarlo yo mismo, pero dado lo que compartiste, simplemente no puedo. ¿Puede proporcionar un ejemplo reproducible mínimo ?
Fureeish
No puedo darle un ejemplo reproducible mínimo en este momento, pero el código en el ejemplo debería ser suficiente para discutir la teoría. Desde un punto de vista teórico, ¿cómo podría finalcambiar la identidad de un argumento de método fuera y dentro de la lambda? ¿Y por qué capturar la variable como una finalvariable local en la pila hace alguna diferencia?
Garret Wilson el
Hoy, capturar la variable dentro del método como una finalvariable en la pila ya no soluciona el problema. Yo sigo investigando.
Garret Wilson el
1
Dado su caso de uso, lo único que se le viene a la mente es la serialización de la lambda; eso explicaría por qué cambia la referencia. ¿También es igual la carga útil? ¿Tiene el mismo contenido que antes?
NoDataFound
1
Tengo la sospecha de que el error aquí no proviene de la captura lambda, sino de SwingActionsí mismo. ¿Qué haces con la SwingActiondevolución del método?
Leo Aso

Respuestas:

2

¿Cómo es posible que un parámetro final pasado por un método y capturado por un lambda pueda cambiar?

No lo es. Como ha señalado, a menos que haya un error en la JVM, esto no puede suceder.

Esto es muy difícil de precisar sin un ejemplo reproducible mínimo. Aquí están las observaciones que ha realizado:

  1. La primera vez que llamo createAction (), los dos métodos System.out.println () imprimen exactamente el mismo código hash, lo que indica que la carga útil capturada dentro del lambda es la misma que pasé a createAction ().
  2. Si luego llamo a createAction () con una carga útil diferente, los dos valores de System.out.println () impresos son diferentes. ¡En particular, la segunda línea impresa siempre indica el mismo valor que se imprimió en el paso 1!

Una posible explicación que se ajusta a la evidencia es que la lambda que se llama la segunda vez es, de hecho, la lambda de la primera ejecución, y la lambda creada por la segunda ejecución se ha descartado. Eso daría las observaciones anteriores y colocaría el error dentro del código que no ha mostrado aquí.

Quizás podría agregar un registro adicional para registrar: a) los identificadores de cualquier lambda creado dentro de createAction en el momento de la creación (creo que necesitaría cambiar las lambdas en clases anon que implementen las interfaces de devolución de llamada con el registro en sus constructores) b) el ID de las lambdas en el momento de su invocación

Creo que el registro anterior sería suficiente para probar o refutar mi teoría.

GL!

Rico
fuente
0

No encuentro nada malo con las lambdas capturadas en su código.

Su solución alternativa no cambia la captura local, ya que acaba de declarar una nueva variable y asignarla a la misma referencia.

El problema es más probable en su manejo del SwingActionobjeto creado. No me sorprendería si encuentra que imprimir el IdentityHashCode de ese objeto devuelto donde lo está utilizando arroja valores consistentes con sus cargas útiles. O en otras palabras, puede estar utilizando una referencia previa a SwingAction.

Además, la pregunta fundamental sigue siendo: ¿cómo es posible que un parámetro final pasado por un método y capturado por una lambda pueda cambiar?

Esto no debería ser posible en el nivel de referencia, la variable no se puede reasignar. Sin embargo, la referencia pasada puede ser mutable, pero eso no se aplica a este caso.

Tomás Fornara
fuente