DEV Community

Maximillian Arruda
Maximillian Arruda

Posted on • Updated on

[PT-BR] Lambda Expressions não são classes anônimas?

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!?
        }
    });
Enter fullscreen mode Exit fullscreen mode

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());
        }
    });
Enter fullscreen mode Exit fullscreen mode

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()));
Enter fullscreen mode Exit fullscreen mode

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

    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();        
    }
}
Enter fullscreen mode Exit fullscreen mode

Muito bem, vamos então conferir as classes compiladas a partir desta nova versão de código:

bin/ $ tree .
.
└── UsingLambdaExpressions.class
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:


  1. Livro: Effective Java - Joshua Bloch

  2. Desing Patterns - Strategy [PT-BR]/[EN-US] | Livro: Design Patterns: Elements of Reusable Object-Oriented Software 

  3. JBang

Discussion (5)

Collapse
wldomiciano profile image
Wellington Domiciano

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.

import java.util.function.Consumer;

public class App {
  public static void main(String... args) {
    Consumer<Integer> consumer1 = e -> {
    };

    Consumer<Integer> consumer2 = new Consumer<Integer>() {
      @Override
      public void accept(Integer t) {
      }
    };

    System.out.println(consumer1.getClass().isAnonymousClass()); // false
    System.out.println(consumer2.getClass().isAnonymousClass()); // true
  }
}
Enter fullscreen mode Exit fullscreen mode

O compilador realmente transforma a lambda em métodos. O código abaixo...

import java.util.stream.Stream;

public class App {
  public static void main(String... args) {
    Stream.of(args)
      .map(e -> Integer.valueOf(e))
      .forEach(e -> System.out.println(e));
  }
}
Enter fullscreen mode Exit fullscreen mode

... Vira isso depois de compilado quando vc roda javap -c -p App:

Compiled from "App.java"
public class App {
  public App();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String...);
    Code:
       0: aload_0
       1: invokestatic  #7                  // InterfaceMethod java/util/stream/Stream.of:([Ljava/lang/Object;)Ljava/util/stream/Stream;
       4: invokedynamic #13,  0             // InvokeDynamic #0:apply:()Ljava/util/function/Function;
       9: invokeinterface #17,  2           // InterfaceMethod java/util/stream/Stream.map:(Ljava/util/function/Function;)Ljava/util/stream/Stream;
      14: invokedynamic #21,  0             // InvokeDynamic #1:accept:()Ljava/util/function/Consumer;
      19: invokeinterface #25,  2           // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V
      24: return

  private static void lambda$main$1(java.lang.Integer);
    Code:
       0: getstatic     #29                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: invokevirtual #35                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
       7: return

  private static java.lang.Integer lambda$main$0(java.lang.String);
    Code:
       0: aload_0
       1: invokestatic  #41                 // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
       4: areturn
}
Enter fullscreen mode Exit fullscreen mode

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:

docs.oracle.com/javase/specs/jls/s...

The value of a lambda expression is a reference to an instance of a class with the following properties:

  • The class implements the targeted functional interface type and, if the target type is an intersection type, every other interface type mentioned in the intersection.
  • Where the lambda expression has type U, for each non-static member method m of U:
  • If the function type of U has a subsignature of the signature of m, then the class declares a method that overrides m. The method's body has the effect of evaluating the lambda > body, if it is an expression, or of executing the lambda body, if it is a block; if a result is expected, it is returned from the method.
  • If the erasure of the type of a method being overridden differs in its signature from the erasure of the function type of U, then before evaluating or executing the lambda body, > the method's body checks that each argument value is an instance of a subclass or subinterface of the erasure of the corresponding parameter type in the function type of U; if > not, a ClassCastException is thrown.
  • The class overrides no other methods of the targeted functional interface type or other interface types mentioned above, although it may override methods of the Object class.

Isso dá pra gente atestar assim:

import java.util.function.Consumer;

public class App {
  public static void main(String... args) {
    Consumer<Integer> consumer = e -> {
    };

    System.out.println(consumer instanceof Consumer); // true
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

java -Djdk.internal.lambda.dumpProxyClasses=. App
Enter fullscreen mode Exit fullscreen mode

O código acima geraria estes arquivos:

.
├── App$$Lambda$1.class
├── App.class
├── App.java
Enter fullscreen mode Exit fullscreen mode

E rodando javap -c -p App\$\$Lambda\$1 dá pra gente ver:

final class App$$Lambda$1 implements java.util.function.Consumer {
  private App$$Lambda$1();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

  public void accept(java.lang.Object);
    Code:
       0: aload_1
       1: checkcast     #14                 // class java/lang/Integer
       4: invokestatic  #20                 // Method App.lambda$main$0:(Ljava/lang/Integer;)V
       7: return
}
Enter fullscreen mode Exit fullscreen mode

Isso tudo é muito interessante! Eu testei usando Java 17.

Collapse
j_a_o_v_c_t_r profile image
João Victor Martins

Wow, excelente. Concordo com o Max. Isso deveria ser um post haha!! E ai, anima?

Collapse
wldomiciano profile image
Wellington Domiciano

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!

Collapse
dearrudam profile image
Maximillian Arruda Author

Que massa!!! Só essa sua resposta daria um ótimo artigo!!! Obrigado por compartilhar!!! Abraços!!!!

Collapse
j_a_o_v_c_t_r profile image
João Victor Martins

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!!