Captura variável mutável do Java 8 lambda a partir do parâmetro do método?

8

Estou usando o AdoptOpenJDK jdk81212-b04no Ubuntu Linux, executando o Eclipse 4.13. Eu tenho um método no Swing que cria uma lambda dentro de uma lambda; ambos provavelmente são chamados em threads separados. É assim (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);
      }
    });

Você pode ver que as lambdas têm várias camadas de profundidade e, eventualmente, a "carga útil" capturada é salva em um arquivo, mais ou menos.

Mas antes de considerar as camadas e os threads, vamos diretamente ao problema:

  1. Na primeira vez que ligo createAction(), os dois System.out.println()métodos imprimem exatamente o mesmo código hash, indicando que a carga capturada dentro do lambda é a mesma para a qual passei createAction().
  2. Se eu ligar mais tarde createAction()com uma carga útil diferente, os dois System.out.println()valores impressos serão diferentes! Em particular, a segunda linha impressa sempre indica o mesmo valor que foi impresso na etapa 1 !!

Eu posso repetir isso repetidamente; a carga útil transmitida continuará recebendo um código hash de identidade diferente, enquanto a segunda linha impressa (dentro do lambda) permanecerá a mesma! Eventualmente, algo clicará e, de repente, os números serão os mesmos novamente, mas eles divergirão com o mesmo comportamento.

De alguma forma, o Java está armazenando em cache o lambda, bem como o argumento que está indo para o lambda? Mas como isso é possível? O payloadargumento é marcado finale, além disso, as capturas de lambda precisam ser efetivamente finais de qualquer maneira!

  • Existe um bug do Java 8 que não reconhece um lambda não deve ser armazenado em cache se a variável capturada tiver vários lambdas de profundidade?
  • Existe um bug do Java 8 que está armazenando em cache argumentos lambdas e lambda entre threads?
  • Ou existe algo que eu não entendo sobre argumentos do método de captura lambda versus variáveis ​​locais do método?

Primeira falha na tentativa de solução alternativa

Ontem, pensei em impedir esse comportamento simplesmente capturando o parâmetro method localmente na pilha 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);
      }
    });

Com essa linha única Data theRealPayload = payload, se, a partir de agora, eu usei em theRealPayloadvez de payloadrepentinamente o bug não aparecer mais, e toda vez que liguei createAction(), as duas linhas impressas indicariam exatamente a mesma instância da variável capturada.

No entanto, hoje essa solução alternativa parou de funcionar.

Problema separado de endereços de correção de bug; Mas por que?

Eu encontrei um bug separado que estava lançando uma exceção dentro showStatusDialogWithWorker(). Basicamente, showStatusDialogWithWorker()é suposto criar o trabalhador (no lambda passado) e mostrar uma caixa de diálogo de status até que o trabalhador seja concluído. Houve um erro que criaria o trabalhador corretamente, mas não conseguiu criar a caixa de diálogo, lançando uma exceção que surgiria e nunca seria pego. Corrigi esse bug para que o showStatusDialogWithWorker()diálogo seja exibido com êxito quando o trabalhador estiver em execução e depois o feche após a conclusão do trabalhador. Agora não posso mais reproduzir o problema de captura lambda.

Mas por que algo interno se showStatusDialogWithWorker()relaciona com o problema? Quando eu estava imprimindo System.identityHashCode()fora e dentro do lambda, e os valores eram diferentes, isso estava acontecendo antes de showStatusDialogWithWorker() ser chamado e antes da exceção ser lançada. Por que uma exceção posterior fará diferença?

Além disso, permanece a questão fundamental: como é possível que um finalparâmetro passado por um método e capturado por um lambda possa mudar?

Garret Wilson
fonte
5
Não tenho certeza se esse é um exemplo completo . Eu adoraria colar o código e inspecioná-lo, mas, considerando o que você compartilhou, simplesmente não posso. Você pode fornecer um exemplo reprodutível mínimo ?
Fureeish 26/11/19
Não posso dar um exemplo mínimo reproduzível no momento, mas o código no exemplo deve ser suficiente para discutir a teoria. Do ponto de vista teórico, como a identidade de um finalargumento de método pode mudar fora e dentro do lambda? E por que capturar a variável como finalvariável local na pilha faria alguma diferença?
Garret Wilson #:
Hoje, capturar a variável dentro do método como uma finalvariável na pilha não resolve mais o problema. Eu continuo investigando.
Garret Wilson #
1
Dado o seu caso de uso, a única coisa que vem à mente é a serialização do lambda; isso explicaria por que a referência é alterada. Também a carga útil é igual? Tem o mesmo conteúdo que antes?
NoDataFound
1
Eu tenho uma suspeita furtiva de que o bug aqui não vem da captura lambda, mas da SwingActionprópria. O que você faz com os SwingActionretornados do método?
Leo Aso

Respostas:

2

como é possível que um parâmetro final passado por um método e capturado por um lambda possa mudar?

Não é. Como você apontou, a menos que haja um erro na JVM, isso não pode acontecer.

Isso é muito difícil de definir sem um exemplo reprodutível mínimo. Aqui estão as observações que você fez:

  1. Na primeira vez que chamo createAction (), os dois métodos System.out.println () imprimem exatamente o mesmo código de hash, indicando que a carga útil capturada dentro do lambda é a mesma que passei para createAction ().
  2. Se eu chamar posteriormente createAction () com uma carga útil diferente, os dois valores System.out.println () impressos serão diferentes! Em particular, a segunda linha impressa sempre indica o mesmo valor que foi impresso na etapa 1 !!

Uma explicação possível que se ajusta à evidência é que o lambda chamado de segunda vez é de fato o lambda da primeira execução e o lambda que foi criado pela segunda execução foi descartado. Isso daria as observações acima e colocaria o erro dentro do código que você não mostrou aqui.

Talvez você possa adicionar algum log extra para registrar: a) os IDs de qualquer lambda criado dentro de createAction no momento da criação (acho que você precisaria alterar os lambdas em classes anon que implementam as interfaces de retorno de chamada com o log em seus construtores) b) ids das lambdas no momento de sua invocação

Eu acho que o registro acima seria suficiente para provar ou refutar minha teoria.

GL!

Rico
fonte
0

Não encontro nada errado com as lambdas capturadas no seu código.

Sua solução alternativa não altera a captura local, pois você acabou de declarar uma nova variável e atribuí-la à mesma referência.

O problema é mais provável no manuseio do SwingActionobjeto criado. Não ficaria surpreso se você descobrir que a impressão do IdentityHashCode desse objeto retornado em que você está usando gera valores consistentes com suas cargas úteis. Ou, em outras palavras, você pode estar usando uma referência anterior a SwingAction.

Além disso, a questão fundamental permanece: como é possível que um parâmetro final passado por um método e capturado por um lambda possa mudar?

Isso não deve ser possível no nível de referência, a variável não pode ser reatribuída. A referência passada pode ser mutável, mas isso não se aplica a este caso.

Tomas Fornara
fonte