Seguindo essa série sobre "Programação Funcional com Java", me deperei com uma dúvida, que talvez não seja só minha, então vamos lá:
Lambda Expressions não são classes anônimas?
Talvez o óbvio pra mim, não seja óbvio para vocês e vice-versa!
Então, nesse texto, gostaria de abordar um pouco sobre Lambda Expression!
Com Java 8, muitas features, como Lambdas Expression, Streams API e Method References nos permitiram a desenvolver com Java de uma maneira diferente, uma maneira funcional. Estamos no Java 18, com o Java 19 batendo em nossa porta, com previsão para setembro/2022, e algumas questões sobre essas features, que apesar de não serem tão novas, pairam em nossas cabeças.
Objetos de Função
Antes dessas novas features introduzidas pelo Java 8, as interfaces (ou, raramente, as classes abstratas) com um único método abstrato eram utilizadas para representar tipos de função. As instâncias dessas classes são conhecidas como objetos de função, e através dessas implementações, tinhamos uma forma para representar funções ou ações em nossas aplicações.1
Desde o JDK 1.1, o principal meio para criar objetos de função era por classes anônimas.
JButton button = new JButton("Click!");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
// that's a nostalgic thing, do ya!?
}
});
Quem nunca implementou algo assim utilizando a API Swing do Java? Tudo bem, este código é nostálgico, talvez ele traga lembranças não tão agradáveis, então, brincadeiras de lado, vamos olhar outro fragmento de código onde precisamos ordenar palavras pelo comprimento:
Collections.sort(words, new Comparator<String>() {
public int compare(String word1, String word2) {
return Integer.compare(word1.length(), word2.length());
}
});
Aqui, instanciamos uma classe anônima e implementamos a lógica exigida pela interface comparator onde, se retornar -1, significa que o comprimento do primeiro argumento nomeado como word1, é menor que o comprimento do segundo argumento nomeado como word2, se retornar 0 significa que ambos argumentos tem o mesmo comprimento e, finalmente, se retornar 1 significa que o comprimento do argumento word1 é maior que o argumento word2. (Desculpem-me, mas sempre tive que consultar o javadoc para implementar essa lógica, ela sempre me confundia... :P !)
Classes anônimas eram adequadas aos padrões clássicos quando eram exigidos a criação de objetos de função, lembram do design pattern Strategy 2?
Mas toda essa verbosidade exigida na utilização das classes anônimas fazia com a programação funcional se tornasse uma possibilidade bem desagradável.
Pois bem, com o Lambda expressions podemos trazer melhorias significativas a esse código:
Collections.sort(words,
(word1, word2) -> Integer.compare(word1.length(), word2.length()));
Todo aquela verbosidade - instanciação e declaração da classe anônima com parenteses e assinatura do método - desapareceu e o comportamento ficou um pouco mais evidente. Agradeçam as expressões lambdas!
Mas o que são expressões lambdas?
No Java 8, interfaces com um único método abstrato agora recebem um tratamento especial. A linguagem agora permite que expressões lambdas, ou apenas lambdas (para abreviar) implemente objetos de função que representam essas interfaces, que agora essas podem ser chamadas de Interfaces Funcionais - sim \o/... podemos falar mais sobre interfaces funcionais em um futuro próximo.
Então podemos dizer que expressão lambdas são funções anônimas que podemos adicionar em nossos códigos afim de representar um objeto de função.
Você pode estar agora se convencendo que, debaixo do capô, as expressões lambdas são convertidas para classes anônimas, não é?
É agora que a coisa fica interessante!
Olhem esse código:
public class UsingAnonymousClasses {
public static void main(String... args) {
Thread th;
th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread [%s] is doing A...".formatted(Thread.currentThread().getName()));
}
});
th.start();
th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread [%s] is doing B...".formatted(Thread.currentThread().getName()));
}
});
th.start();
th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread [%s] is doing C...".formatted(Thread.currentThread().getName()));
}
});
th.start();
}
}
Por ora, ignore a orquestração da execução das threads, ok ?
Neste código, podemos ver que implementamos 3 (três) classes anônimas com comportamentos distintos.
Para desencargo de consciência, vamos listar as classes compiladas a partir deste código:
bin/ $ tree .
.
├── UsingAnonymousClasses$1.class
├── UsingAnonymousClasses$2.class
├── UsingAnonymousClasses$3.class
└── UsingAnonymousClasses.class
Agora vamos criar uma versão e utilizar lambdas ao invés de classes anônimas:
public class UsingLambdaExpressions {
public static void main(String... args) {
Thread th;
th = new Thread(() -> System.out.println("thread [%s] is doing A...".formatted(Thread.currentThread().getName())));
th.start();
th = new Thread(() -> System.out.println("thread [%s] is doing B...".formatted(Thread.currentThread().getName())));
th.start();
th = new Thread(() -> System.out.println("thread [%s] is doing C...".formatted(Thread.currentThread().getName())));
th.start();
}
}
Muito bem, vamos então conferir as classes compiladas a partir desta nova versão de código:
bin/ $ tree .
.
└── UsingLambdaExpressions.class
Como podemos ver, na verdade, as expressões lambdas não são convertidas para classes anônimas, logo, elas não são classes anônimas!!!
Mas como elas são implementadas afinal!?
Utilizando o utilitário javap podemos dar uma espiada no bytecode gerado:
bin/ $ javap -c UsingLambdaExpressions.class
Compiled from "UsingLambdaExpressions.java"
public class UsingLambdaExpressions {
public UsingLambdaExpressions();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String...);
Code:
0: new #16 // class java/lang/Thread
3: dup
4: invokedynamic #18, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #22 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: astore_1
13: aload_1
14: invokevirtual #25 // Method java/lang/Thread.start:()V
17: new #16 // class java/lang/Thread
20: dup
21: invokedynamic #28, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
26: invokespecial #22 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
29: astore_1
30: aload_1
31: invokevirtual #25 // Method java/lang/Thread.start:()V
34: new #16 // class java/lang/Thread
37: dup
38: invokedynamic #29, 0 // InvokeDynamic #2:run:()Ljava/lang/Runnable;
43: invokespecial #22 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
46: astore_1
47: aload_1
48: invokevirtual #25 // Method java/lang/Thread.start:()V
51: return
}
Curiosamente, não precisamos ter tanto conhecimento para interpretar bytecodes em java para conseguir ver que as expressões lambdas se tornaram instruções chamadas InvokeDynamic.
Mas e o corpo das lambdas?!
Dependendo do contexto, o compilador irá converter o corpo das lambdas em um desses possiveis casos:
- Ou em métodos estáticos (static methods) da própria classe
- Ou métodos vinculado a instâncias definidas (instance methods)
- Ou simplesmente rotear a chamada para métodos existente em uma outras classes.
Interessante não é!?
Bom, agradeço e muito ao "Venkat Subramaniam", pois em uma palestra que ele apresentou no Devoxx em 2015 me ensinou muito! Super recomendo! Segue o link da palestra: Get a Taste of Lambdas and Get Addicted to Streams by Venkat Subramaniam.
Até o próximo artigo!!!
Source dos exemplos 3:
Referências:
-
Livro: Effective Java - Joshua Bloch; ↩
-
Desing Patterns - Strategy [PT-BR]/[EN-US] | Livro: Design Patterns: Elements of Reusable Object-Oriented Software ↩
Top comments (5)
Nossa, Max, acho que eu nunca tinha me feito esta pergunta e seu post foi uma excelente provocação e ponto de partida!
Pesquisando além, olha o que eu descobri:
Como vc disse, expressões lambda não são classes anônimas e o próprio Java nos informa isto.
O compilador realmente transforma a lambda em métodos. O código abaixo...
... Vira isso depois de compilado quando vc roda
javap -c -p App
:Só que não pára por aí. No runtime, segundo a especificação, o valor de uma lambda é uma referência a uma instância de uma classe que implementa a interface funcional desejada. Veja:
Isso dá pra gente atestar assim:
Só que a parte mais legal é que eu descobri através desta resposta que é possível ver como a classe gerada é no runtime usando o seguinte commando:
O código acima geraria estes arquivos:
E rodando
javap -c -p App\$\$Lambda\$1
dá pra gente ver:Isso tudo é muito interessante! Eu testei usando Java 17.
Wow, excelente. Concordo com o Max. Isso deveria ser um post haha!! E ai, anima?
Valeu! Seria legal escrever sobre, eu gostaria de tentar, mas eu sou enrolado para escrever.
Bem que o Max podia fazer uma parte 2 ou mesmo incrementer este post mostrando a parada do
-Djdk.internal.lambda.dumpProxyClasses
porque eu achei bem massa poder ver estes detalhes!Que massa!!! Só essa sua resposta daria um ótimo artigo!!! Obrigado por compartilhar!!! Abraços!!!!
Muito bom, Max. Gostei desse ponto "Como podemos ver, na verdade, as expressões lambdas não são convertidas para classes anônimas, logo, elas não são classes anônimas!!!". Aqui a realidade é que vc pode substituir classes anônimas por lambdas, mas não necessariamente são a mesma coisa, certo? =) excelente conteúdo como sempre. Ansioso pelos próximos!!