Sobes.tech
Middle+

Como funcionam as expressões lambda e o que acontece 'por baixo do capô'?

sobes.tech IA

Resposta da IA

As expressões lambda em Java funcionam através dos seguintes mecanismos:

  1. Interfaces funcionais: A expressão lambda deve ser atribuída a uma variável, devolvida de um método ou passada para um método cujo tipo é uma interface funcional. Uma interface funcional é uma interface com exatamente um método abstrato (SAM - Single Abstract Method). A anotação @FunctionalInterface é opcional, mas recomendada para a verificação pelo compilador.

  2. Invocação dinâmica (invokeDynamic): Em vez de gerar uma classe interna anónima no bytecode na fase de compilação, como era feito em versões antigas, Java usa a instrução invokeDynamic. Esta instrução foi adicionada no Java 7 para suportar linguagens dinamicamente tipadas na JVM, e no Java 8 é usada para implementar expressões lambda e referências a métodos. invokeDynamic adia a resolução da chamada de método até ao momento de execução (runtime).

O que acontece 'por baixo do capô':

Na fase de compilação:

  • O compilador analisa a expressão lambda.
  • Determina o tipo da interface funcional à qual a lambda se refere.
  • Gera um método sintético especial (frequentemente com o prefixo lambda$) na mesma classe que contém a lógica da lambda. Este método tem a mesma assinatura que o método abstrato da interface funcional.
  • No local onde a lambda é usada, o compilador gera a instrução invokeDynamic. Esta instrução contém uma referência ao bootstrap method (LambdaMetafactory.metafactory).

Na fase de execução (runtime):

  • Quando a JVM encontra a instrução invokeDynamic, ela chama pela primeira vez o bootstrap method (LambdaMetafactory.metafactory).
  • O bootstrap method gera dinamicamente na memória um call site.
  • O call site cria e devolve uma instância de uma classe que implementa a interface funcional. Esta instância pode ser:
    • Criada em tempo de execução (por exemplo, usando um Constant CallSite gerado).
    • Em cache para uso posterior (para lambdas sem estado).
    • Representada como uma referência a uma instância já existente da interface funcional (por exemplo, para lambdas sem estado que implementam Serializable).
  • Esta classe gerada delega a chamada ao método abstrato da interface funcional ao método sintético gerado pelo compilador (lambda$).
  • Após a primeira chamada ao bootstrap method, o call site lembra qual objeto deve devolver, e chamadas subsequentes a invokeDynamic para a mesma lambda podem obter rapidamente esse objeto ou chamá-lo diretamente sem re-invocar o bootstrap method.

Assim, as lambdas nem sempre criam uma nova classe a cada instância. Para lambdas que não capturam variáveis do contexto externo (lambdas sem estado), a implementação da interface funcional pode ser um singleton ou uma instância que é cacheada e reutilizada. Lambdas que capturam variáveis (lambdas com estado) requerem a criação de uma nova instância de classe que armazenará as variáveis capturadas.

O uso de invokeDynamic permite que o compilador JIT otimize a execução de chamadas de métodos de interfaces funcionais de forma mais eficiente do que o modelo tradicional de classes anónimas, pois o JIT pode tomar decisões mais informadas em tempo de execução.

// Exemplo de interface funcional
@FunctionalInterface
interface MyFunction {
    String apply(String input);
}

public class LambdaExample {
    public static void main(String[] args) {
        // Uso de expressão lambda
        MyFunction func = s -> "Processed: " + s;

        String result = func.apply("Test");
        System.out.println(result);
    }
}