Entenda de vez as expressões lambda em Java

Se você já utilizou expressões lambda em Java mas não entendeu completamente como elas funcionam ou deseja entender seu funcionamento, este post é para você!

Por Gabriel Luciano

Criado em 10 de setembro de 2023 às 00:14

Imagem do post

Talvez você já tenha utilizado ou utilize as expressões lambda em Java sem entender completamente de onde elas vêm ou como elas funcionam. Afinal, como uma linguagem que não tem funções pode aceitar uma "função" como parâmetro de um método? Ou talvez você esteja apenas querendo aprender a utilizá-las. Seja qual for o seu caso, este post é para você. Vamos partir de um exemplo simples inicialmente, sem utilizar as expressões lambdas, e então refatorá-lo para utilizá-las. Assim, você não só irá aprender a sintaxe das expressões lambdas, mas também irá entender a lógica por trás delas e como criá-las.

Introduzindo o exemplo

Neste exemplo, vamos implementar uma classe simples chamada Botao. Nela, vamos utilizar um padrão de projetos conhecido como Observer, onde uma instância de botão possuirá uma lista de observadores que serão notificados sempre que o botão for clicado.

public class Botao {

  private List<Observador> observadores = new ArrayList<>();

  public Botao() {
  }

  public void clicar() {
    // Lógica
    this.notificarObservadores("Botão Clicado!");
  }

  private void notificarObservadores(String mensagem) {
    for (Observador observador : observadores) {
      observador.botaoClicado(mensagem);
    }
  }

  public void adicionarObservador(Observador observador) {
    this.observadores.add(observador);
  }
}

A classe Botao pode ser um elemento de uma interface. A lista de observadores é definida em List<Observador> observadores e inicia vazia. Para adicionar um novo observador, utilizamos o método adicionarObservador. Por fim, sempre que botão for clicado através do método clicar, notificamos todos os observadores chamando o método notificarObservadores e passamos a eles uma mensagem. Note que os observadores devem implementar a interface Observador, que contém o método botaoClicado.

public interface Observador {

  void botaoClicado(String mensagem);

}

Suponha que temos interesse em criar um observador do botão cuja função será exibir a mensagem recebida no console. Possivelmente você já tem a solução em mente. Para isso, podemos criar uma classe que implementa a interface Observador e implementar o método botaoClicado com um simples System.out.println.

public class ConsoleObservador implements Observador {

  @Override
  public void botaoClicado(String mensagem) {
    System.out.println("Mensagem recebida: " + mensagem);
  }

}

Pronto, agora que temos o nosso ConsoleObservador criado, basta colocarmos tudo para funcionar! Criamos uma instância de botão, depois criamos uma instância de ConsoleObservador e adicionamos ao botão.

public class Main {
    public static void main(String[] args) {
        Botao botao = new Botao();
        Observador observador = new ConsoleObservador();
        botao.adicionarObservador(observador);

        botao.clicar(); // Mensagem recebida: Botão Clicado!
    }
}

Executando o código acima, teremos o resultado esperado que é a mensagem "Mensagem recebida: Botão Clicado!" exibida no console. Agora vamos partir para a primeira refatoração deste código.

Primeira refatoração: Utilizando Classes Anônimas

Como vimos no exemplo acima, foi necessário criar uma classe chamada ConsoleObservador simplesmente para logar o evento de botão clicado no console. Uma forma de eliminar a necessidade de uso dessa classe é utilizando Classes Anônimas que são uma forma diferente de definir uma classe que implementa uma interface. Neste caso, o nosso código ficará desta forma:

public class Main {
    public static void main(String[] args) {
        Botao botao = new Botao();

        botao.adicionarObservador(new Observador() {

            @Override
            public void botaoClicado(String mensagem) {
                System.out.println("Mensagem recebida: " + mensagem);
            }

        });

        botao.clicar(); // Mensagem recebida: Botão Clicado!
    }
}

Como você pode ver no exemplo, em vez de criarmos uma instância da classe ConsoleObservador, definimos uma classe e implementamos o método botaoClicado diretamente dentro do método adicionarObservador. A notação new Observador() {} significa que estamos declarando e instanciando uma Classe que implementa a interface Observador de uma única vez.

Uma outra forma de reescrever o código acima, é adicionando a classe anônima à uma variável.

public static void main(String[] args) {
    Botao botao = new Botao();

    Observador observador = new Observador() {
        @Override
        public void botaoClicado(String mensagem) {
            System.out.println("Mensagem recebida: " + mensagem);
        }
    };

    botao.adicionarObservador(observador);
    botao.clicar(); // Mensagem recebida: Botão Clicado!
}

Segunda refatoração: Utilizando Lambda Expressions!

Finalmente, chegou a parte que todos esperavam: vamos refatorar este código para utilizar o poder das Lambda Expressions ou Expressões Lambda!

Primeiramente, vamos realizar um pequeno ajuste na nossa interface Observador, adicionando a ela a anotação @FunctionalInterface.


@FunctionalInterface
public interface Observador {

  void botaoClicado(String mensagem);

}

Mas o que é uma Interface Funcional?

As Interfaces Funcionais são pontos chaves para a utilização das Lambda Expressions, e na verdade são muito simples, sendo nada mais do que interfaces com um único método abstrato. Este método é chamado de método funcional. Ao definir uma interface funcional, não necessariamente precisamos utilizar a anotação @FunctionalInterface, mas é uma boa prática fazê-la, pois assim nossa IDE irá nos avisar caso adicionemos mais de um método abstrato em uma interface que deveria ser funcional. Do ponto de vista do compilador esta anotação não fará diferença pois ele será capaz de identificar quais interfaces satisfazem as condições para serem consideradas Functional Interfaces ou não.

Agora que entendemos que nossa interface Observador satisfaz as condições de uma interface funcional, podemos refatorar o nosso código para utilizar as expressões lambda. Uma expressão lambda nada mais é do que uma implementação do método funcional definido na nossa interface funcional, no nosso exemplo, o método void botaoClicado(String mensagem).

public class Main {
    public static void main(String[] args) {
        Botao botao = new Botao();
        botao.adicionarObservador(msg -> System.out.println("Mensagem recebida: " + msg));
        botao.clicar(); // Mensagem recebida: Botão Clicado!
    }
}

Destrinchando o código acima:

  • Sabemos que o método adicionarObservador recebe um objeto do tipo Observador
  • Também sabemos que a nossa interface Observador é uma interface funcional que possui um único método botaoClicado
  • Desta forma, podemos utilizar uma lambda expression definida como:
(param1, param2...) -> { Corpo da expressão }
  • No nosso caso, temos um único parâmetro msg, que equivale ao parâmetro String mensagem do método botaoClicado. Neste caso, a utilização dos parênteses é opcional.
  • Do lado direito da seta temos o corpo da expressão, que pode ou não vir entre chaves. Caso as chaves não sejam utilizadas a expressão será tratada como o retorno do método. As chaves são úteis quando precisamos definir mais de uma instrução, neste caso precisaremos definir explicitamente o retorno do método para métodos não void. Veja os dois exemplos abaixo:
private static void test() {

    //  Utilizando chaves (retorno explícito)
    double result = this.calcular(10, 5, (a, b) -> {
        return a + b;
    });

    System.out.println(result); // 15.0
}
private static void test() {

    // Sem uso de chaves (retorno implícito)
    double result = this.calcular(10, 5, (a, b) -> a + b);

    System.out.println(result); // 15.0
}

Interfaces funcionais do pacote java.util.function

Embora possamos definir nossas próprias interfaces funcionais, podemos também utilizar as diversas interfaces funcionais que estão disponíveis no pacote java.util.function. Essas interfaces funcionais utilizam Generics e possuem métodos com diversas assinaturas diferentes. É muito provável que você encontrará o método funcional que está procurando em uma dessas interfaces. Assim, poderá diminuir ainda mais a quantidade de código no seu projeto!

Alguns exemplos dessas interfaces são BinaryOperator, Consumer e Function. No nosso exemplo deste post, a interface Observador poderia ser facilmente substituída pela interface funcional Consumer<T> com o tipo String. Veja como ficaria nosso código:

import java.util.function.Consumer;

public class Botao {

  private List<Consumer<String>> observadores = new ArrayList<>();

  public Botao() {
  }

  public void clicar() {
    // Lógica
    this.notificarObservadores("Botão Clicado!");
  }

  private void notificarObservadores(String mensagem) {
    for (Consumer<String> observador : observadores) {
      observador.accept(mensagem);
    }
  }

  public void adicionarObservador(Consumer<String> observador) {
    this.observadores.add(observador);
  }
}

Veja que o método funcional da interface Consumer é chamado accept. Já no nosso método main, nenhuma mudança será necessária.

public class Main {
    public static void main(String[] args) {
        Botao botao = new Botao();
        botao.adicionarObservador(msg -> System.out.println("Mensagem recebida: " + msg));
        botao.clicar(); // Mensagem recebida: Botão Clicado!
    }
}

Com isso, eliminamos a necessidade de criar a nossa interface Observador, diminuindo a quantidade de código necessário em nosso projeto!

Conclusão

Por hoje é só pessoal! Claro que ainda existem muitos outros assuntos relacionados às expressões lambda, mas espero que este post tenha sido útil para você entender como elas funcionam e como aplicá-las em seus projetos. Se você tem alguma dúvida ou sugestão de melhoria sobre este post utilize a seção de comentários relacionadas a este post no Linkedin. Até a próxima!