CodeGym /Blogue Java /Random-PT /Uma explicação das expressões lambda em Java. Com exemplo...
John Squirrels
Nível 41
San Francisco

Uma explicação das expressões lambda em Java. Com exemplos e tarefas. Parte 2

Publicado no grupo Random-PT
Para quem é este artigo?
  • É para quem leu a primeira parte deste artigo;
  • É para pessoas que acham que já conhecem bem o Java Core, mas não têm ideia sobre expressões lambda em Java. Ou talvez eles tenham ouvido algo sobre expressões lambda, mas faltam detalhes.
  • É para pessoas que têm uma certa compreensão das expressões lambda, mas ainda se assustam com elas e não estão acostumadas a usá-las.
Se você não se encaixa em nenhuma dessas categorias, pode achar este artigo chato, falho ou geralmente não é sua preferência. Nesse caso, fique à vontade para passar para outras coisas ou, se você for bem versado no assunto, dê sugestões nos comentários sobre como eu poderia melhorar ou complementar o artigo. Uma explicação das expressões lambda em Java.  Com exemplos e tarefas.  Parte 2 - 1O material não pretende ter nenhum valor acadêmico, muito menos novidade. Muito pelo contrário: tentarei descrever coisas que são complexas (para algumas pessoas) da maneira mais simples possível. Uma solicitação para explicar a Stream API me inspirou a escrever isso. Eu pensei sobre isso e decidi que alguns dos meus exemplos de stream seriam incompreensíveis sem uma compreensão das expressões lambda. Então, vamos começar com expressões lambda.

Acesso a variáveis ​​externas

Este código compila com uma classe anônima?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
Não. A counter variável deve ser final. Ou, se não for final, pelo menos não pode alterar seu valor. O mesmo princípio se aplica às expressões lambda. Eles podem acessar todas as variáveis ​​que podem "ver" do local em que são declaradas. Mas um lambda não deve alterá-los (atribuir um novo valor a eles). No entanto, existe uma maneira de contornar essa restrição em classes anônimas. Simplesmente crie uma variável de referência e altere o estado interno do objeto. Ao fazer isso, a própria variável não muda (aponta para o mesmo objeto) e pode ser marcada com segurança como final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Aqui nossa countervariável é uma referência a um AtomicIntegerobjeto. E o incrementAndGet()método é usado para alterar o estado desse objeto. O valor da própria variável não muda durante a execução do programa. Sempre aponta para o mesmo objeto, o que nos permite declarar a variável com a palavra-chave final. Aqui estão os mesmos exemplos, mas com expressões lambda:

int counter = 0;
Runnable r = () -> counter++;
Isso não compilará pelo mesmo motivo da versão com uma classe anônima:  counternão deve mudar enquanto o programa estiver em execução. Mas está tudo bem se fizermos assim:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Isso também se aplica a métodos de chamada. Nas expressões lambda, você pode não apenas acessar todas as variáveis ​​"visíveis", mas também chamar quaisquer métodos acessíveis.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Embora staticMethod()seja privado, é acessível dentro do main()método, portanto também pode ser chamado de dentro de um lambda criado no mainmétodo.

Quando uma expressão lambda é executada?

Você pode achar a seguinte pergunta muito simples, mas deve fazê-la da mesma forma: quando o código dentro da expressão lambda será executado? Quando é criado? Ou quando é chamado (o que ainda não se sabe)? Isso é bastante fácil de verificar.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Saída da tela:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Você pode ver que a expressão lambda foi executada bem no final, depois que o thread foi criado e somente quando a execução do programa atinge o run()método. Certamente não quando é declarado. Ao declarar uma expressão lambda, criamos apenas um Runnableobjeto e descrevemos como seu run()método se comporta. O próprio método é executado muito mais tarde.

Referências de método?

Referências de métodos não estão diretamente relacionadas a lambdas, mas acho que faz sentido dizer algumas palavras sobre eles neste artigo. Suponha que temos uma expressão lambda que não faz nada de especial, mas simplesmente chama um método.

x -> System.out.println(x)
Recebe alguns xe apenas chama System.out.println(), repassando x. Nesse caso, podemos substituí-lo por uma referência ao método desejado. Assim:

System.out::println
Isso mesmo — sem parênteses no final! Aqui está um exemplo mais completo:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
Na última linha, utilizamos o forEach()método, que recebe um objeto que implementa a Consumerinterface. Novamente, esta é uma interface funcional, com apenas um void accept(T t)método. Assim, escrevemos uma expressão lambda que possui um parâmetro (por ser digitado na própria interface, não especificamos o tipo do parâmetro, apenas indicamos que iremos chamá-lo x). No corpo da expressão lambda, escrevemos o código que será executado quando o accept()método for chamado. Aqui, simplesmente exibimos o que acabou na xvariável. Esse mesmo forEach()método itera por todos os elementos da coleção e chama o accept()método na implementação doConsumerinterface (nosso lambda), passando em cada item da coleção. Como eu disse, podemos substituir tal expressão lambda (uma que simplesmente classifica um método diferente) por uma referência ao método desejado. Então nosso código ficará assim:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
O principal é que os parâmetros dos métodos println()e accept()correspondam. Como o println()método pode aceitar qualquer coisa (está sobrecarregado para todos os tipos primitivos e todos os objetos), em vez de expressões lambda, podemos simplesmente passar uma referência ao println()método para forEach(). Em seguida forEach(), pegará cada elemento da coleção e o passará diretamente para o println()método. Para quem encontra isso pela primeira vez, observe que não estamos ligando System.out.println()(com pontos entre as palavras e com parênteses no final). Em vez disso, estamos passando uma referência a esse método. Se escrevermos isso

strings.forEach(System.out.println());
teremos um erro de compilação. Antes da chamada para forEach(), o Java vê que System.out.println()está sendo chamado, então entende que o valor de retorno é voide tentará passar voidpara forEach(), que está esperando um Consumerobjeto.

Sintaxe para referências de método

É bem simples:
  1. Passamos uma referência para um método estático como este:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. Passamos uma referência a um método não estático usando um objeto existente, assim:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. Passamos uma referência a um método não estático usando a classe que o implementa da seguinte forma:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. Passamos uma referência para um construtor como este:ClassName::new

    As referências de método são muito convenientes quando você já tem um método que funcionaria perfeitamente como um retorno de chamada. Nesse caso, em vez de escrever uma expressão lambda contendo o código do método ou escrever uma expressão lambda que simplesmente chama o método, simplesmente passamos uma referência a ela. E é isso.

Uma distinção interessante entre classes anônimas e expressões lambda

Em uma classe anônima, a thispalavra-chave aponta para um objeto da classe anônima. Mas se usarmos isso dentro de um lambda, ganhamos acesso ao objeto da classe que o contém. Aquele em que realmente escrevemos a expressão lambda. Isso acontece porque as expressões lambda são compiladas em um método privado da classe em que estão escritas. Eu não recomendaria usar esse "recurso", pois tem um efeito colateral e contraria os princípios da programação funcional. Dito isso, essa abordagem é totalmente consistente com OOP. ;)

Onde obtive minhas informações e o que mais você deve ler?

E, claro, encontrei um monte de coisas no Google :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION